super_auth 0.1.4 → 0.2.0

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/Gemfile +7 -10
  4. data/Gemfile.lock +53 -5
  5. data/LICENSE.txt +125 -21
  6. data/README.md +32 -1
  7. data/Rakefile +0 -2
  8. data/USAGE.md +619 -0
  9. data/VISUALIZATION.md +58 -0
  10. data/app/controllers/super_auth/graph_controller.rb +661 -0
  11. data/app/views/super_auth/graph/index.html.erb +1408 -0
  12. data/config/routes.rb +73 -0
  13. data/db/migrate/1_users.rb +7 -4
  14. data/db/migrate/2_groups.rb +14 -3
  15. data/db/migrate/3_permissions.rb +6 -2
  16. data/db/migrate/4_roles.rb +14 -3
  17. data/db/migrate/5_resources.rb +7 -4
  18. data/db/migrate/6_edge.rb +6 -4
  19. data/db/migrate/7_authorization.rb +41 -0
  20. data/db/migrate/8_add_indexes_to_edges.rb +17 -0
  21. data/db/migrate_activerecord/20250101000001_create_super_auth_users.rb +10 -0
  22. data/db/migrate_activerecord/20250101000002_create_super_auth_groups.rb +11 -0
  23. data/db/migrate_activerecord/20250101000003_create_super_auth_permissions.rb +8 -0
  24. data/db/migrate_activerecord/20250101000004_create_super_auth_roles.rb +11 -0
  25. data/db/migrate_activerecord/20250101000005_create_super_auth_resources.rb +10 -0
  26. data/db/migrate_activerecord/20250101000006_create_super_auth_edges.rb +12 -0
  27. data/db/migrate_activerecord/20250101000007_create_super_auth_authorizations.rb +41 -0
  28. data/db/seeds/sample_data.rb +193 -0
  29. data/lib/basic_loader.rb +10 -2
  30. data/lib/generators/super_auth/install/install_generator.rb +19 -0
  31. data/lib/generators/super_auth/install/templates/README +29 -0
  32. data/lib/generators/super_auth/install/templates/super_auth.rb +7 -0
  33. data/lib/super_auth/active_record/authorization.rb +3 -0
  34. data/lib/super_auth/active_record/by_current_user.rb +39 -0
  35. data/lib/super_auth/active_record/edge.rb +48 -0
  36. data/lib/super_auth/active_record/group.rb +10 -0
  37. data/lib/super_auth/active_record/permission.rb +7 -0
  38. data/lib/super_auth/active_record/resource.rb +4 -0
  39. data/lib/super_auth/active_record/role.rb +10 -0
  40. data/lib/super_auth/active_record/user.rb +14 -0
  41. data/lib/super_auth/active_record.rb +20 -0
  42. data/lib/super_auth/authorization.rb +2 -0
  43. data/lib/super_auth/edge.rb +205 -92
  44. data/lib/super_auth/group.rb +1 -0
  45. data/lib/super_auth/nestable.rb +17 -10
  46. data/lib/super_auth/permission.rb +1 -1
  47. data/lib/super_auth/railtie.rb +30 -0
  48. data/lib/super_auth/role.rb +2 -1
  49. data/lib/super_auth/user.rb +14 -14
  50. data/lib/super_auth/version.rb +1 -3
  51. data/lib/super_auth.rb +103 -29
  52. data/lib/tasks/super_auth_tasks.rake +9 -8
  53. data/visualization.html +747 -0
  54. metadata +33 -6
@@ -0,0 +1,1408 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Interactive SuperAuth Graph</title>
5
+ <script src="https://d3js.org/d3.v7.min.js"></script>
6
+ <style>
7
+ body {
8
+ margin: 0;
9
+ padding: 20px;
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
11
+ background: #1a1a1a;
12
+ color: #fff;
13
+ }
14
+
15
+ .container {
16
+ display: flex;
17
+ gap: 20px;
18
+ height: 95vh;
19
+ }
20
+
21
+ .sidebar {
22
+ width: 350px;
23
+ background: #2a2a2a;
24
+ border-radius: 8px;
25
+ padding: 20px;
26
+ overflow-y: auto;
27
+ }
28
+
29
+ .graph-container {
30
+ flex: 1;
31
+ background: #2a2a2a;
32
+ border-radius: 8px;
33
+ position: relative;
34
+ }
35
+
36
+ h1, h2, h3 {
37
+ margin-top: 0;
38
+ }
39
+
40
+ .section {
41
+ margin-bottom: 30px;
42
+ }
43
+
44
+ .form-group {
45
+ margin-bottom: 15px;
46
+ }
47
+
48
+ label {
49
+ display: block;
50
+ margin-bottom: 5px;
51
+ font-size: 14px;
52
+ color: #aaa;
53
+ }
54
+
55
+ input, select {
56
+ width: 100%;
57
+ padding: 8px 12px;
58
+ background: #1a1a1a;
59
+ border: 1px solid #444;
60
+ border-radius: 4px;
61
+ color: #fff;
62
+ font-size: 14px;
63
+ box-sizing: border-box;
64
+ }
65
+
66
+ button {
67
+ padding: 10px 20px;
68
+ background: #0066cc;
69
+ color: white;
70
+ border: none;
71
+ border-radius: 4px;
72
+ cursor: pointer;
73
+ font-size: 14px;
74
+ width: 100%;
75
+ }
76
+
77
+ button:hover {
78
+ background: #0052a3;
79
+ }
80
+
81
+ button.delete {
82
+ background: #cc0000;
83
+ }
84
+
85
+ button.delete:hover {
86
+ background: #a30000;
87
+ }
88
+
89
+ .entity-list {
90
+ max-height: 200px;
91
+ overflow-y: auto;
92
+ background: #1a1a1a;
93
+ border-radius: 4px;
94
+ padding: 10px;
95
+ margin-top: 10px;
96
+ }
97
+
98
+ .entity-item {
99
+ display: flex;
100
+ justify-content: space-between;
101
+ align-items: center;
102
+ padding: 8px;
103
+ margin-bottom: 5px;
104
+ background: #2a2a2a;
105
+ border-radius: 4px;
106
+ }
107
+
108
+ .entity-item button {
109
+ width: auto;
110
+ padding: 4px 12px;
111
+ font-size: 12px;
112
+ }
113
+
114
+ .entity-item span[onclick]:hover,
115
+ .entity-item span[onclick^="highlightEdgeFromList"]:hover {
116
+ color: #ffd700;
117
+ text-decoration: underline;
118
+ }
119
+
120
+ .stats {
121
+ display: grid;
122
+ grid-template-columns: repeat(2, 1fr);
123
+ gap: 10px;
124
+ margin-bottom: 20px;
125
+ }
126
+
127
+ .stat-box {
128
+ background: #1a1a1a;
129
+ padding: 15px;
130
+ border-radius: 4px;
131
+ text-align: center;
132
+ }
133
+
134
+ .stat-number {
135
+ font-size: 24px;
136
+ font-weight: bold;
137
+ color: #0066cc;
138
+ }
139
+
140
+ .stat-label {
141
+ font-size: 12px;
142
+ color: #aaa;
143
+ margin-top: 5px;
144
+ }
145
+
146
+ svg {
147
+ width: 100%;
148
+ height: 100%;
149
+ }
150
+
151
+ .node {
152
+ cursor: move;
153
+ }
154
+
155
+ .node circle {
156
+ stroke: #fff;
157
+ stroke-width: 2px;
158
+ }
159
+
160
+ .node.pinned circle {
161
+ stroke-dasharray: 3,3;
162
+ }
163
+
164
+ .node text {
165
+ font-size: 12px;
166
+ fill: #fff;
167
+ text-anchor: middle;
168
+ pointer-events: none;
169
+ }
170
+
171
+ .link {
172
+ stroke: #666;
173
+ stroke-width: 2px;
174
+ stroke-opacity: 0.6;
175
+ fill: none;
176
+ cursor: pointer;
177
+ }
178
+
179
+ .link.highlighted {
180
+ stroke: #ffd700;
181
+ stroke-width: 3px;
182
+ stroke-opacity: 1;
183
+ }
184
+
185
+ .node.highlighted circle {
186
+ stroke: #ffd700;
187
+ stroke-width: 4px;
188
+ filter: drop-shadow(0 0 8px #ffd700);
189
+ }
190
+
191
+ .node.dimmed {
192
+ opacity: 0.2;
193
+ }
194
+
195
+ .link.dimmed {
196
+ opacity: 0.1;
197
+ }
198
+
199
+ .message {
200
+ position: fixed;
201
+ top: 20px;
202
+ right: 20px;
203
+ padding: 15px 20px;
204
+ border-radius: 4px;
205
+ background: #0066cc;
206
+ color: white;
207
+ z-index: 1000;
208
+ animation: slideIn 0.3s ease;
209
+ }
210
+
211
+ .message.error {
212
+ background: #cc0000;
213
+ }
214
+
215
+ @keyframes slideIn {
216
+ from {
217
+ transform: translateX(400px);
218
+ opacity: 0;
219
+ }
220
+ to {
221
+ transform: translateX(0);
222
+ opacity: 1;
223
+ }
224
+ }
225
+
226
+ .tabs {
227
+ display: flex;
228
+ gap: 10px;
229
+ margin-bottom: 20px;
230
+ }
231
+
232
+ .tab {
233
+ padding: 8px 16px;
234
+ background: #1a1a1a;
235
+ border-radius: 4px;
236
+ cursor: pointer;
237
+ font-size: 14px;
238
+ }
239
+
240
+ .tab.active {
241
+ background: #0066cc;
242
+ }
243
+
244
+ .tab-content {
245
+ display: none;
246
+ }
247
+
248
+ .tab-content.active {
249
+ display: block;
250
+ }
251
+
252
+ .orphan-item {
253
+ padding: 10px;
254
+ background: #2a2a2a;
255
+ border-radius: 4px;
256
+ margin-bottom: 8px;
257
+ border-left: 3px solid #ff9800;
258
+ }
259
+
260
+ .orphan-item .name {
261
+ font-weight: bold;
262
+ margin-bottom: 4px;
263
+ }
264
+
265
+ .orphan-item .reason {
266
+ font-size: 12px;
267
+ color: #aaa;
268
+ }
269
+
270
+ .legend {
271
+ position: absolute;
272
+ top: 20px;
273
+ right: 20px;
274
+ background: rgba(42, 42, 42, 0.95);
275
+ border: 1px solid #444;
276
+ border-radius: 8px;
277
+ padding: 15px;
278
+ z-index: 100;
279
+ }
280
+
281
+ .legend h4 {
282
+ margin: 0 0 10px 0;
283
+ font-size: 14px;
284
+ font-weight: 600;
285
+ }
286
+
287
+ .legend-item {
288
+ display: flex;
289
+ align-items: center;
290
+ gap: 10px;
291
+ margin-bottom: 8px;
292
+ }
293
+
294
+ .legend-item:last-child {
295
+ margin-bottom: 0;
296
+ }
297
+
298
+ .legend-color {
299
+ width: 16px;
300
+ height: 16px;
301
+ border-radius: 50%;
302
+ flex-shrink: 0;
303
+ }
304
+
305
+ .legend-label {
306
+ font-size: 13px;
307
+ }
308
+ </style>
309
+ </head>
310
+ <body>
311
+ <div class="container">
312
+ <div class="sidebar">
313
+ <h1>SuperAuth Graph</h1>
314
+
315
+ <!-- Filters Section -->
316
+ <div class="section">
317
+ <h3>Filters</h3>
318
+ <div class="form-group">
319
+ <label for="filter-user">User</label>
320
+ <select id="filter-user" onchange="applyFilters()">
321
+ <option value="">All Users</option>
322
+ </select>
323
+ </div>
324
+ <div class="form-group">
325
+ <label for="filter-resource">Resource</label>
326
+ <select id="filter-resource" onchange="applyFilters()">
327
+ <option value="">All Resources</option>
328
+ </select>
329
+ </div>
330
+ <button onclick="clearFilters()" style="width: 100%; padding: 8px; background: #444; border: none; color: white; border-radius: 4px; cursor: pointer;">Clear Filters</button>
331
+ </div>
332
+
333
+ <div class="stats">
334
+ <div class="stat-box">
335
+ <div class="stat-number" id="user-count">0</div>
336
+ <div class="stat-label">Users</div>
337
+ </div>
338
+ <div class="stat-box">
339
+ <div class="stat-number" id="group-count">0</div>
340
+ <div class="stat-label">Groups</div>
341
+ </div>
342
+ <div class="stat-box">
343
+ <div class="stat-number" id="role-count">0</div>
344
+ <div class="stat-label">Roles</div>
345
+ </div>
346
+ <div class="stat-box">
347
+ <div class="stat-number" id="permission-count">0</div>
348
+ <div class="stat-label">Permissions</div>
349
+ </div>
350
+ <div class="stat-box">
351
+ <div class="stat-number" id="resource-count">0</div>
352
+ <div class="stat-label">Resources</div>
353
+ </div>
354
+ <div class="stat-box">
355
+ <div class="stat-number" id="edge-count">0</div>
356
+ <div class="stat-label">Edges</div>
357
+ </div>
358
+ </div>
359
+
360
+ <div class="section">
361
+ <button onclick="compileAuthorizations()" style="background: #9C27B0;">
362
+ Compile Authorizations
363
+ </button>
364
+ <p style="font-size: 12px; color: #aaa; margin-top: 10px;">
365
+ Analyzes all edges and populates the authorizations table with complete user-to-resource paths for auditing.
366
+ </p>
367
+ </div>
368
+
369
+ <div class="tabs">
370
+ <div class="tab active" data-tab="add">Add</div>
371
+ <div class="tab" data-tab="manage">Manage</div>
372
+ <div class="tab" data-tab="orphans">Orphans</div>
373
+ </div>
374
+
375
+ <div class="tab-content active" id="add-tab">
376
+ <div class="section">
377
+ <h3>Add User</h3>
378
+ <form id="add-user-form">
379
+ <div class="form-group">
380
+ <label>Name</label>
381
+ <input type="text" name="name" required>
382
+ </div>
383
+ <button type="submit">Add User</button>
384
+ </form>
385
+ </div>
386
+
387
+ <div class="section">
388
+ <h3>Add Group</h3>
389
+ <form id="add-group-form">
390
+ <div class="form-group">
391
+ <label>Name</label>
392
+ <input type="text" name="name" required>
393
+ </div>
394
+ <div class="form-group">
395
+ <label>Parent Group (optional)</label>
396
+ <select name="parent_id" id="parent-group-select">
397
+ <option value="">None</option>
398
+ </select>
399
+ </div>
400
+ <button type="submit">Add Group</button>
401
+ </form>
402
+ </div>
403
+
404
+ <div class="section">
405
+ <h3>Add Role</h3>
406
+ <form id="add-role-form">
407
+ <div class="form-group">
408
+ <label>Name</label>
409
+ <input type="text" name="name" required>
410
+ </div>
411
+ <div class="form-group">
412
+ <label>Parent Role (optional)</label>
413
+ <select name="parent_id" id="parent-role-select">
414
+ <option value="">None</option>
415
+ </select>
416
+ </div>
417
+ <button type="submit">Add Role</button>
418
+ </form>
419
+ </div>
420
+
421
+ <div class="section">
422
+ <h3>Add Permission</h3>
423
+ <form id="add-permission-form">
424
+ <div class="form-group">
425
+ <label>Name</label>
426
+ <input type="text" name="name" required>
427
+ </div>
428
+ <button type="submit">Add Permission</button>
429
+ </form>
430
+ </div>
431
+
432
+ <div class="section">
433
+ <h3>Add Resource</h3>
434
+ <form id="add-resource-form">
435
+ <div class="form-group">
436
+ <label>Name</label>
437
+ <input type="text" name="name" required>
438
+ </div>
439
+ <button type="submit">Add Resource</button>
440
+ </form>
441
+ </div>
442
+
443
+ <div class="section">
444
+ <h3>Add Edge (Connection)</h3>
445
+ <form id="add-edge-form">
446
+ <div class="form-group">
447
+ <label>User</label>
448
+ <select name="user_id" id="edge-user-select">
449
+ <option value="">None</option>
450
+ </select>
451
+ </div>
452
+ <div class="form-group">
453
+ <label>Group</label>
454
+ <select name="group_id" id="edge-group-select">
455
+ <option value="">None</option>
456
+ </select>
457
+ </div>
458
+ <div class="form-group">
459
+ <label>Role</label>
460
+ <select name="role_id" id="edge-role-select">
461
+ <option value="">None</option>
462
+ </select>
463
+ </div>
464
+ <div class="form-group">
465
+ <label>Permission</label>
466
+ <select name="permission_id" id="edge-permission-select">
467
+ <option value="">None</option>
468
+ </select>
469
+ </div>
470
+ <div class="form-group">
471
+ <label>Resource</label>
472
+ <select name="resource_id" id="edge-resource-select">
473
+ <option value="">None</option>
474
+ </select>
475
+ </div>
476
+ <button type="submit">Add Edge</button>
477
+ </form>
478
+ </div>
479
+ </div>
480
+
481
+ <div class="tab-content" id="manage-tab">
482
+ <div class="section">
483
+ <h3>Users</h3>
484
+ <div class="entity-list" id="users-list"></div>
485
+ </div>
486
+
487
+ <div class="section">
488
+ <h3>Groups</h3>
489
+ <div class="entity-list" id="groups-list"></div>
490
+ </div>
491
+
492
+ <div class="section">
493
+ <h3>Roles</h3>
494
+ <div class="entity-list" id="roles-list"></div>
495
+ </div>
496
+
497
+ <div class="section">
498
+ <h3>Permissions</h3>
499
+ <div class="entity-list" id="permissions-list"></div>
500
+ </div>
501
+
502
+ <div class="section">
503
+ <h3>Resources</h3>
504
+ <div class="entity-list" id="resources-list"></div>
505
+ </div>
506
+
507
+ <div class="section">
508
+ <h3>Edges</h3>
509
+ <div class="entity-list" id="edges-list"></div>
510
+ </div>
511
+ </div>
512
+
513
+ <div class="tab-content" id="orphans-tab">
514
+ <p style="color: #aaa; margin-bottom: 20px;">
515
+ Orphaned records are entities not part of any complete authorization path between a user and a resource.
516
+ </p>
517
+
518
+ <button onclick="loadOrphans()" style="margin-bottom: 20px;">
519
+ Refresh Orphans
520
+ </button>
521
+
522
+ <div class="section">
523
+ <h3>Orphaned Users (<span id="orphan-users-count">0</span>)</h3>
524
+ <div class="entity-list" id="orphan-users-list"></div>
525
+ </div>
526
+
527
+ <div class="section">
528
+ <h3>Orphaned Groups (<span id="orphan-groups-count">0</span>)</h3>
529
+ <div class="entity-list" id="orphan-groups-list"></div>
530
+ </div>
531
+
532
+ <div class="section">
533
+ <h3>Orphaned Roles (<span id="orphan-roles-count">0</span>)</h3>
534
+ <div class="entity-list" id="orphan-roles-list"></div>
535
+ </div>
536
+
537
+ <div class="section">
538
+ <h3>Orphaned Permissions (<span id="orphan-permissions-count">0</span>)</h3>
539
+ <div class="entity-list" id="orphan-permissions-list"></div>
540
+ </div>
541
+
542
+ <div class="section">
543
+ <h3>Orphaned Resources (<span id="orphan-resources-count">0</span>)</h3>
544
+ <div class="entity-list" id="orphan-resources-list"></div>
545
+ </div>
546
+ </div>
547
+ </div>
548
+
549
+ <div class="graph-container">
550
+ <svg id="graph-svg"></svg>
551
+
552
+ <!-- Legend -->
553
+ <div class="legend">
554
+ <h4>Node Types</h4>
555
+ <div class="legend-item">
556
+ <div class="legend-color" style="background: #4CAF50;"></div>
557
+ <div class="legend-label">User</div>
558
+ </div>
559
+ <div class="legend-item">
560
+ <div class="legend-color" style="background: #2196F3;"></div>
561
+ <div class="legend-label">Group</div>
562
+ </div>
563
+ <div class="legend-item">
564
+ <div class="legend-color" style="background: #FF9800;"></div>
565
+ <div class="legend-label">Role</div>
566
+ </div>
567
+ <div class="legend-item">
568
+ <div class="legend-color" style="background: #9C27B0;"></div>
569
+ <div class="legend-label">Permission</div>
570
+ </div>
571
+ <div class="legend-item">
572
+ <div class="legend-color" style="background: #F44336;"></div>
573
+ <div class="legend-label">Resource</div>
574
+ </div>
575
+ </div>
576
+ </div>
577
+ </div>
578
+
579
+ <script>
580
+ // Get the engine mount path - request.script_name contains the mount point
581
+ const basePath = '<%= request.script_name %>';
582
+
583
+ let graphData = { users: [], groups: [], roles: [], permissions: [], resources: [], edges: [] };
584
+ let highlightNodeById = null; // Will be set by renderGraph
585
+ let highlightEdgeByData = null; // Will be set by renderGraph
586
+
587
+ // Colors for different node types
588
+ const colors = {
589
+ user: '#4CAF50',
590
+ group: '#2196F3',
591
+ role: '#FF9800',
592
+ permission: '#9C27B0',
593
+ resource: '#F44336'
594
+ };
595
+
596
+ // Tab switching
597
+ document.querySelectorAll('.tab').forEach(tab => {
598
+ tab.addEventListener('click', () => {
599
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
600
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
601
+ tab.classList.add('active');
602
+ document.getElementById(tab.dataset.tab + '-tab').classList.add('active');
603
+ });
604
+ });
605
+
606
+ // Show message
607
+ function showMessage(text, isError = false) {
608
+ const msg = document.createElement('div');
609
+ msg.className = 'message' + (isError ? ' error' : '');
610
+ msg.textContent = text;
611
+ document.body.appendChild(msg);
612
+ setTimeout(() => msg.remove(), 3000);
613
+ }
614
+
615
+ // Load graph data
616
+ async function loadGraph() {
617
+ try {
618
+ const response = await fetch(basePath + '/graph/data');
619
+ graphData = await response.json();
620
+ updateStats();
621
+ updateSelects();
622
+ updateLists();
623
+ renderGraph();
624
+ } catch (error) {
625
+ showMessage('Error loading graph: ' + error.message, true);
626
+ }
627
+ }
628
+
629
+ // Apply filters (client-side)
630
+ function applyFilters() {
631
+ renderGraph();
632
+ }
633
+
634
+ // Clear filters
635
+ function clearFilters() {
636
+ document.getElementById('filter-user').value = '';
637
+ document.getElementById('filter-resource').value = '';
638
+ renderGraph();
639
+ }
640
+
641
+ // Update statistics
642
+ function updateStats() {
643
+ document.getElementById('user-count').textContent = graphData.users.length;
644
+ document.getElementById('group-count').textContent = graphData.groups.length;
645
+ document.getElementById('role-count').textContent = graphData.roles.length;
646
+ document.getElementById('permission-count').textContent = graphData.permissions.length;
647
+ document.getElementById('resource-count').textContent = graphData.resources.length;
648
+ document.getElementById('edge-count').textContent = graphData.edges.length;
649
+ }
650
+
651
+ // Update select dropdowns
652
+ function updateSelects() {
653
+ // Filter dropdowns - preserve current selection
654
+ const filterUser = document.getElementById('filter-user');
655
+ const currentUserFilter = filterUser.value;
656
+ filterUser.innerHTML = '<option value="">All Users</option>';
657
+ graphData.users.forEach(u => {
658
+ filterUser.innerHTML += `<option value="${u.id}"${currentUserFilter == u.id ? ' selected' : ''}>${u.name}</option>`;
659
+ });
660
+
661
+ const filterResource = document.getElementById('filter-resource');
662
+ const currentResourceFilter = filterResource.value;
663
+ filterResource.innerHTML = '<option value="">All Resources</option>';
664
+ graphData.resources.forEach(r => {
665
+ filterResource.innerHTML += `<option value="${r.id}"${currentResourceFilter == r.id ? ' selected' : ''}>${r.name}</option>`;
666
+ });
667
+
668
+ // Parent group select
669
+ const parentGroupSelect = document.getElementById('parent-group-select');
670
+ parentGroupSelect.innerHTML = '<option value="">None</option>';
671
+ graphData.groups.forEach(g => {
672
+ parentGroupSelect.innerHTML += `<option value="${g.id}">${g.name}</option>`;
673
+ });
674
+
675
+ // Parent role select
676
+ const parentRoleSelect = document.getElementById('parent-role-select');
677
+ parentRoleSelect.innerHTML = '<option value="">None</option>';
678
+ graphData.roles.forEach(r => {
679
+ parentRoleSelect.innerHTML += `<option value="${r.id}">${r.name}</option>`;
680
+ });
681
+
682
+ // Edge selects
683
+ const edgeUserSelect = document.getElementById('edge-user-select');
684
+ edgeUserSelect.innerHTML = '<option value="">None</option>';
685
+ graphData.users.forEach(u => {
686
+ edgeUserSelect.innerHTML += `<option value="${u.id}">${u.name}</option>`;
687
+ });
688
+
689
+ const edgeGroupSelect = document.getElementById('edge-group-select');
690
+ edgeGroupSelect.innerHTML = '<option value="">None</option>';
691
+ graphData.groups.forEach(g => {
692
+ edgeGroupSelect.innerHTML += `<option value="${g.id}">${g.name}</option>`;
693
+ });
694
+
695
+ const edgeRoleSelect = document.getElementById('edge-role-select');
696
+ edgeRoleSelect.innerHTML = '<option value="">None</option>';
697
+ graphData.roles.forEach(r => {
698
+ edgeRoleSelect.innerHTML += `<option value="${r.id}">${r.name}</option>`;
699
+ });
700
+
701
+ const edgePermissionSelect = document.getElementById('edge-permission-select');
702
+ edgePermissionSelect.innerHTML = '<option value="">None</option>';
703
+ graphData.permissions.forEach(p => {
704
+ edgePermissionSelect.innerHTML += `<option value="${p.id}">${p.name}</option>`;
705
+ });
706
+
707
+ const edgeResourceSelect = document.getElementById('edge-resource-select');
708
+ edgeResourceSelect.innerHTML = '<option value="">None</option>';
709
+ graphData.resources.forEach(r => {
710
+ edgeResourceSelect.innerHTML += `<option value="${r.id}">${r.name}</option>`;
711
+ });
712
+ }
713
+
714
+ // Update entity lists
715
+ function updateLists() {
716
+ // Users list
717
+ const usersList = document.getElementById('users-list');
718
+ usersList.innerHTML = '';
719
+ graphData.users.forEach(u => {
720
+ usersList.innerHTML += `
721
+ <div class="entity-item">
722
+ <span onclick="highlightEntityFromList('user', ${u.id})" style="cursor: pointer; flex: 1;">${u.name}</span>
723
+ <button class="delete" onclick="deleteEntity('users', ${u.id})">Delete</button>
724
+ </div>
725
+ `;
726
+ });
727
+
728
+ // Groups list
729
+ const groupsList = document.getElementById('groups-list');
730
+ groupsList.innerHTML = '';
731
+ graphData.groups.forEach(g => {
732
+ const parentInfo = g.parent_id ? ` (parent: ${graphData.groups.find(p => p.id === g.parent_id)?.name || 'unknown'})` : '';
733
+ groupsList.innerHTML += `
734
+ <div class="entity-item">
735
+ <span onclick="highlightEntityFromList('group', ${g.id})" style="cursor: pointer; flex: 1;">${g.name}${parentInfo}</span>
736
+ <button class="delete" onclick="deleteEntity('groups', ${g.id})">Delete</button>
737
+ </div>
738
+ `;
739
+ });
740
+
741
+ // Roles list
742
+ const rolesList = document.getElementById('roles-list');
743
+ rolesList.innerHTML = '';
744
+ graphData.roles.forEach(r => {
745
+ const parentInfo = r.parent_id ? ` (parent: ${graphData.roles.find(p => p.id === r.parent_id)?.name || 'unknown'})` : '';
746
+ rolesList.innerHTML += `
747
+ <div class="entity-item">
748
+ <span onclick="highlightEntityFromList('role', ${r.id})" style="cursor: pointer; flex: 1;">${r.name}${parentInfo}</span>
749
+ <button class="delete" onclick="deleteEntity('roles', ${r.id})">Delete</button>
750
+ </div>
751
+ `;
752
+ });
753
+
754
+ // Permissions list
755
+ const permissionsList = document.getElementById('permissions-list');
756
+ permissionsList.innerHTML = '';
757
+ graphData.permissions.forEach(p => {
758
+ permissionsList.innerHTML += `
759
+ <div class="entity-item">
760
+ <span onclick="highlightEntityFromList('permission', ${p.id})" style="cursor: pointer; flex: 1;">${p.name}</span>
761
+ <button class="delete" onclick="deleteEntity('permissions', ${p.id})">Delete</button>
762
+ </div>
763
+ `;
764
+ });
765
+
766
+ // Resources list
767
+ const resourcesList = document.getElementById('resources-list');
768
+ resourcesList.innerHTML = '';
769
+ graphData.resources.forEach(r => {
770
+ resourcesList.innerHTML += `
771
+ <div class="entity-item">
772
+ <span onclick="highlightEntityFromList('resource', ${r.id})" style="cursor: pointer; flex: 1;">${r.name}</span>
773
+ <button class="delete" onclick="deleteEntity('resources', ${r.id})">Delete</button>
774
+ </div>
775
+ `;
776
+ });
777
+
778
+ // Edges list
779
+ const edgesList = document.getElementById('edges-list');
780
+ edgesList.innerHTML = '';
781
+ graphData.edges.forEach(e => {
782
+ const parts = [];
783
+ if (e.user_id) parts.push(`U:${graphData.users.find(u => u.id === e.user_id)?.name || e.user_id}`);
784
+ if (e.group_id) parts.push(`G:${graphData.groups.find(g => g.id === e.group_id)?.name || e.group_id}`);
785
+ if (e.role_id) parts.push(`R:${graphData.roles.find(r => r.id === e.role_id)?.name || e.role_id}`);
786
+ if (e.permission_id) parts.push(`P:${graphData.permissions.find(p => p.id === e.permission_id)?.name || e.permission_id}`);
787
+ if (e.resource_id) parts.push(`Res:${graphData.resources.find(r => r.id === e.resource_id)?.name || e.resource_id}`);
788
+
789
+ const edgeDataJson = JSON.stringify(e).replace(/"/g, '&quot;');
790
+ edgesList.innerHTML += `
791
+ <div class="entity-item">
792
+ <span onclick='highlightEdgeFromList(${edgeDataJson})' style="font-size: 11px; flex: 1; cursor: pointer;">${parts.join(' - ')}</span>
793
+ <button class="delete" onclick="deleteEntity('edges', ${e.id})">Delete</button>
794
+ </div>
795
+ `;
796
+ });
797
+ }
798
+
799
+ // Highlight entity from list click
800
+ function highlightEntityFromList(type, id) {
801
+ if (highlightNodeById) {
802
+ highlightNodeById(type, id);
803
+ }
804
+ }
805
+
806
+ // Highlight edge from list click
807
+ function highlightEdgeFromList(edgeData) {
808
+ if (highlightEdgeByData) {
809
+ highlightEdgeByData(edgeData);
810
+ }
811
+ }
812
+
813
+ // Render graph with D3
814
+ function renderGraph() {
815
+ const svg = d3.select('#graph-svg');
816
+ svg.selectAll('*').remove();
817
+
818
+ const width = document.querySelector('.graph-container').clientWidth;
819
+ const height = document.querySelector('.graph-container').clientHeight;
820
+
821
+ // Create a container group for zoom/pan
822
+ const g = svg.append('g');
823
+
824
+ // Add zoom behavior
825
+ const zoom = d3.zoom()
826
+ .scaleExtent([0.1, 4])
827
+ .on('zoom', (event) => {
828
+ g.attr('transform', event.transform);
829
+ });
830
+
831
+ svg.call(zoom);
832
+
833
+ // Apply client-side filters
834
+ // Create nodes array - backend already filtered the data
835
+ const nodes = [
836
+ ...graphData.users.map(u => ({ ...u, type: 'user' })),
837
+ ...graphData.groups.map(g => ({ ...g, type: 'group' })),
838
+ ...graphData.roles.map(r => ({ ...r, type: 'role' })),
839
+ ...graphData.permissions.map(p => ({ ...p, type: 'permission' })),
840
+ ...graphData.resources.map(r => ({ ...r, type: 'resource' }))
841
+ ];
842
+
843
+ // Create links array from edges
844
+ const links = [];
845
+ graphData.edges.forEach(edge => {
846
+ const connectedNodes = [];
847
+ if (edge.user_id) connectedNodes.push({ id: edge.user_id, type: 'user' });
848
+ if (edge.group_id) connectedNodes.push({ id: edge.group_id, type: 'group' });
849
+ if (edge.role_id) connectedNodes.push({ id: edge.role_id, type: 'role' });
850
+ if (edge.permission_id) connectedNodes.push({ id: edge.permission_id, type: 'permission' });
851
+ if (edge.resource_id) connectedNodes.push({ id: edge.resource_id, type: 'resource' });
852
+
853
+ for (let i = 0; i < connectedNodes.length - 1; i++) {
854
+ links.push({
855
+ source: `${connectedNodes[i].type}-${connectedNodes[i].id}`,
856
+ target: `${connectedNodes[i + 1].type}-${connectedNodes[i + 1].id}`,
857
+ edgeId: edge.id
858
+ });
859
+ }
860
+ });
861
+
862
+ // Add parent-child links for groups and roles
863
+ graphData.groups.forEach(g => {
864
+ if (g.parent_id) {
865
+ links.push({
866
+ source: `group-${g.parent_id}`,
867
+ target: `group-${g.id}`,
868
+ isHierarchy: true
869
+ });
870
+ }
871
+ });
872
+
873
+ graphData.roles.forEach(r => {
874
+ if (r.parent_id) {
875
+ links.push({
876
+ source: `role-${r.parent_id}`,
877
+ target: `role-${r.id}`,
878
+ isHierarchy: true
879
+ });
880
+ }
881
+ });
882
+
883
+ // Create simulation
884
+ const simulation = d3.forceSimulation(nodes)
885
+ .force('link', d3.forceLink(links).id(d => `${d.type}-${d.id}`).distance(100))
886
+ .force('charge', d3.forceManyBody().strength(-300))
887
+ .force('center', d3.forceCenter(width / 2, height / 2))
888
+ .force('collision', d3.forceCollide().radius(40));
889
+
890
+ // Create links
891
+ const link = g.append('g')
892
+ .selectAll('line')
893
+ .data(links)
894
+ .join('line')
895
+ .attr('class', 'link')
896
+ .style('stroke-dasharray', d => d.isHierarchy ? '5,5' : 'none')
897
+ .on('click', function(event, d) {
898
+ event.stopPropagation();
899
+ highlightEdge(d);
900
+ });
901
+
902
+ // Create nodes
903
+ const node = g.append('g')
904
+ .selectAll('g')
905
+ .data(nodes)
906
+ .join('g')
907
+ .attr('class', 'node')
908
+ .on('click', function(event, d) {
909
+ event.stopPropagation();
910
+ highlightNode(d, this);
911
+ })
912
+ .on('dblclick', function(event, d) {
913
+ event.stopPropagation();
914
+ // Double-click to unpin node
915
+ d.fx = null;
916
+ d.fy = null;
917
+ d3.select(this).classed('pinned', false);
918
+ simulation.alpha(0.3).restart();
919
+ })
920
+ .call(d3.drag()
921
+ .on('start', dragstarted)
922
+ .on('drag', dragged)
923
+ .on('end', dragended));
924
+
925
+ node.append('circle')
926
+ .attr('r', 20)
927
+ .attr('fill', d => colors[d.type]);
928
+
929
+ node.append('text')
930
+ .attr('dy', 30)
931
+ .text(d => d.name);
932
+
933
+ // Click on background to clear highlights
934
+ svg.on('click', clearHighlights);
935
+
936
+ // Highlight functionality
937
+ let selectedNode = null;
938
+ let selectedEdge = null;
939
+
940
+ function highlightNode(d, nodeElement) {
941
+ // If clicking the same node, clear highlights
942
+ if (selectedNode && selectedNode.id === d.id && selectedNode.type === d.type) {
943
+ clearHighlights();
944
+ return;
945
+ }
946
+
947
+ selectedNode = d;
948
+ selectedEdge = null;
949
+ const nodeId = `${d.type}-${d.id}`;
950
+
951
+ // Find all connected nodes
952
+ const connectedNodeIds = new Set([nodeId]);
953
+ const connectedLinks = [];
954
+
955
+ links.forEach(link => {
956
+ const sourceId = typeof link.source === 'object' ? `${link.source.type}-${link.source.id}` : link.source;
957
+ const targetId = typeof link.target === 'object' ? `${link.target.type}-${link.target.id}` : link.target;
958
+
959
+ if (sourceId === nodeId || targetId === nodeId) {
960
+ connectedNodeIds.add(sourceId);
961
+ connectedNodeIds.add(targetId);
962
+ connectedLinks.push(link);
963
+ }
964
+ });
965
+
966
+ // Dim all nodes and links
967
+ node.classed('dimmed', true).classed('highlighted', false);
968
+ link.classed('dimmed', true).classed('highlighted', false);
969
+
970
+ // Highlight selected node and connected nodes
971
+ node.classed('highlighted', function(n) {
972
+ return connectedNodeIds.has(`${n.type}-${n.id}`);
973
+ }).classed('dimmed', function(n) {
974
+ return !connectedNodeIds.has(`${n.type}-${n.id}`);
975
+ });
976
+
977
+ // Highlight connected links
978
+ link.classed('highlighted', function(l) {
979
+ return connectedLinks.includes(l);
980
+ }).classed('dimmed', function(l) {
981
+ return !connectedLinks.includes(l);
982
+ });
983
+ }
984
+
985
+ function highlightEdge(d) {
986
+ // If clicking the same edge, clear highlights
987
+ if (selectedEdge === d) {
988
+ clearHighlights();
989
+ return;
990
+ }
991
+
992
+ selectedEdge = d;
993
+ selectedNode = null;
994
+
995
+ const sourceId = typeof d.source === 'object' ? `${d.source.type}-${d.source.id}` : d.source;
996
+ const targetId = typeof d.target === 'object' ? `${d.target.type}-${d.target.id}` : d.target;
997
+
998
+ // Highlight only the two nodes connected by this edge
999
+ const connectedNodeIds = new Set([sourceId, targetId]);
1000
+
1001
+ // Dim all nodes and links
1002
+ node.classed('dimmed', true).classed('highlighted', false);
1003
+ link.classed('dimmed', true).classed('highlighted', false);
1004
+
1005
+ // Highlight only the two connected nodes
1006
+ node.classed('highlighted', function(n) {
1007
+ return connectedNodeIds.has(`${n.type}-${n.id}`);
1008
+ }).classed('dimmed', function(n) {
1009
+ return !connectedNodeIds.has(`${n.type}-${n.id}`);
1010
+ });
1011
+
1012
+ // Highlight only this edge
1013
+ link.classed('highlighted', function(l) {
1014
+ return l === d;
1015
+ }).classed('dimmed', function(l) {
1016
+ return l !== d;
1017
+ });
1018
+ }
1019
+
1020
+ function clearHighlights() {
1021
+ selectedNode = null;
1022
+ selectedEdge = null;
1023
+ node.classed('highlighted', false).classed('dimmed', false);
1024
+ link.classed('highlighted', false).classed('dimmed', false);
1025
+ }
1026
+
1027
+ // Expose highlight function for external use
1028
+ highlightNodeById = function(type, id) {
1029
+ const nodeData = nodes.find(n => n.type === type && n.id === id);
1030
+ if (nodeData) {
1031
+ highlightNode(nodeData);
1032
+ }
1033
+ };
1034
+
1035
+ // Expose edge highlight function for external use
1036
+ highlightEdgeByData = function(edgeData) {
1037
+ // Find all links that are part of this edge
1038
+ const matchingLinks = links.filter(l => l.edgeId === edgeData.id);
1039
+
1040
+ if (matchingLinks.length === 0) return;
1041
+
1042
+ // Clear previous selection
1043
+ selectedEdge = null;
1044
+ selectedNode = null;
1045
+
1046
+ // Find all nodes that are part of this edge
1047
+ const connectedNodeIds = new Set();
1048
+ matchingLinks.forEach(link => {
1049
+ const sourceId = typeof link.source === 'object' ? `${link.source.type}-${link.source.id}` : link.source;
1050
+ const targetId = typeof link.target === 'object' ? `${link.target.type}-${link.target.id}` : link.target;
1051
+ connectedNodeIds.add(sourceId);
1052
+ connectedNodeIds.add(targetId);
1053
+ });
1054
+
1055
+ // Dim all nodes and links
1056
+ node.classed('dimmed', true).classed('highlighted', false);
1057
+ link.classed('dimmed', true).classed('highlighted', false);
1058
+
1059
+ // Highlight connected nodes
1060
+ node.classed('highlighted', function(n) {
1061
+ return connectedNodeIds.has(`${n.type}-${n.id}`);
1062
+ }).classed('dimmed', function(n) {
1063
+ return !connectedNodeIds.has(`${n.type}-${n.id}`);
1064
+ });
1065
+
1066
+ // Highlight all links that are part of this edge
1067
+ link.classed('highlighted', function(l) {
1068
+ return matchingLinks.includes(l);
1069
+ }).classed('dimmed', function(l) {
1070
+ return !matchingLinks.includes(l);
1071
+ });
1072
+ };
1073
+
1074
+ // Update positions on simulation tick
1075
+ simulation.on('tick', () => {
1076
+ link
1077
+ .attr('x1', d => d.source.x)
1078
+ .attr('y1', d => d.source.y)
1079
+ .attr('x2', d => d.target.x)
1080
+ .attr('y2', d => d.target.y);
1081
+
1082
+ node.attr('transform', d => `translate(${d.x},${d.y})`);
1083
+ });
1084
+
1085
+ // Drag functions
1086
+ function dragstarted(event, d) {
1087
+ if (!event.active) simulation.alphaTarget(0.3).restart();
1088
+ d.fx = d.x;
1089
+ d.fy = d.y;
1090
+ }
1091
+
1092
+ function dragged(event, d) {
1093
+ d.fx = event.x;
1094
+ d.fy = event.y;
1095
+ }
1096
+
1097
+ function dragended(event, d) {
1098
+ if (!event.active) simulation.alphaTarget(0);
1099
+ // Keep fx and fy set so the node stays where it was placed
1100
+ // Mark node as pinned with visual indicator
1101
+ node.filter(n => n === d).classed('pinned', true);
1102
+ }
1103
+ }
1104
+
1105
+ // Delete entity
1106
+ async function deleteEntity(type, id) {
1107
+ if (!confirm(`Are you sure you want to delete this ${type.slice(0, -1)}?`)) return;
1108
+
1109
+ try {
1110
+ const response = await fetch(basePath + `/graph/${type}/${id}`, {
1111
+ method: 'DELETE',
1112
+ headers: { 'Content-Type': 'application/json' }
1113
+ });
1114
+ const result = await response.json();
1115
+
1116
+ if (result.success) {
1117
+ showMessage(`${type.slice(0, -1)} deleted successfully`);
1118
+ loadGraph();
1119
+ } else {
1120
+ showMessage(result.error, true);
1121
+ }
1122
+ } catch (error) {
1123
+ showMessage('Error: ' + error.message, true);
1124
+ }
1125
+ }
1126
+
1127
+ // Form submissions
1128
+ document.getElementById('add-user-form').addEventListener('submit', async (e) => {
1129
+ e.preventDefault();
1130
+ const formData = new FormData(e.target);
1131
+
1132
+ try {
1133
+ const response = await fetch(basePath + '/graph/users', {
1134
+ method: 'POST',
1135
+ headers: { 'Content-Type': 'application/json' },
1136
+ body: JSON.stringify({ user: Object.fromEntries(formData) })
1137
+ });
1138
+ const result = await response.json();
1139
+
1140
+ if (result.success) {
1141
+ showMessage('User added successfully');
1142
+ e.target.reset();
1143
+ loadGraph();
1144
+ } else {
1145
+ showMessage(result.error, true);
1146
+ }
1147
+ } catch (error) {
1148
+ showMessage('Error: ' + error.message, true);
1149
+ }
1150
+ });
1151
+
1152
+ document.getElementById('add-group-form').addEventListener('submit', async (e) => {
1153
+ e.preventDefault();
1154
+ const formData = new FormData(e.target);
1155
+ const data = Object.fromEntries(formData);
1156
+ if (!data.parent_id) delete data.parent_id;
1157
+
1158
+ try {
1159
+ const response = await fetch(basePath + '/graph/groups', {
1160
+ method: 'POST',
1161
+ headers: { 'Content-Type': 'application/json' },
1162
+ body: JSON.stringify({ group: data })
1163
+ });
1164
+ const result = await response.json();
1165
+
1166
+ if (result.success) {
1167
+ showMessage('Group added successfully');
1168
+ e.target.reset();
1169
+ loadGraph();
1170
+ } else {
1171
+ showMessage(result.error, true);
1172
+ }
1173
+ } catch (error) {
1174
+ showMessage('Error: ' + error.message, true);
1175
+ }
1176
+ });
1177
+
1178
+ document.getElementById('add-role-form').addEventListener('submit', async (e) => {
1179
+ e.preventDefault();
1180
+ const formData = new FormData(e.target);
1181
+ const data = Object.fromEntries(formData);
1182
+ if (!data.parent_id) delete data.parent_id;
1183
+
1184
+ try {
1185
+ const response = await fetch(basePath + '/graph/roles', {
1186
+ method: 'POST',
1187
+ headers: { 'Content-Type': 'application/json' },
1188
+ body: JSON.stringify({ role: data })
1189
+ });
1190
+ const result = await response.json();
1191
+
1192
+ if (result.success) {
1193
+ showMessage('Role added successfully');
1194
+ e.target.reset();
1195
+ loadGraph();
1196
+ } else {
1197
+ showMessage(result.error, true);
1198
+ }
1199
+ } catch (error) {
1200
+ showMessage('Error: ' + error.message, true);
1201
+ }
1202
+ });
1203
+
1204
+ document.getElementById('add-permission-form').addEventListener('submit', async (e) => {
1205
+ e.preventDefault();
1206
+ const formData = new FormData(e.target);
1207
+
1208
+ try {
1209
+ const response = await fetch(basePath + '/graph/permissions', {
1210
+ method: 'POST',
1211
+ headers: { 'Content-Type': 'application/json' },
1212
+ body: JSON.stringify({ permission: Object.fromEntries(formData) })
1213
+ });
1214
+ const result = await response.json();
1215
+
1216
+ if (result.success) {
1217
+ showMessage('Permission added successfully');
1218
+ e.target.reset();
1219
+ loadGraph();
1220
+ } else {
1221
+ showMessage(result.error, true);
1222
+ }
1223
+ } catch (error) {
1224
+ showMessage('Error: ' + error.message, true);
1225
+ }
1226
+ });
1227
+
1228
+ document.getElementById('add-resource-form').addEventListener('submit', async (e) => {
1229
+ e.preventDefault();
1230
+ const formData = new FormData(e.target);
1231
+
1232
+ try {
1233
+ const response = await fetch(basePath + '/graph/resources', {
1234
+ method: 'POST',
1235
+ headers: { 'Content-Type': 'application/json' },
1236
+ body: JSON.stringify({ resource: Object.fromEntries(formData) })
1237
+ });
1238
+ const result = await response.json();
1239
+
1240
+ if (result.success) {
1241
+ showMessage('Resource added successfully');
1242
+ e.target.reset();
1243
+ loadGraph();
1244
+ } else {
1245
+ showMessage(result.error, true);
1246
+ }
1247
+ } catch (error) {
1248
+ showMessage('Error: ' + error.message, true);
1249
+ }
1250
+ });
1251
+
1252
+ document.getElementById('add-edge-form').addEventListener('submit', async (e) => {
1253
+ e.preventDefault();
1254
+ const formData = new FormData(e.target);
1255
+ const data = {};
1256
+
1257
+ // Only include non-empty values
1258
+ for (const [key, value] of formData.entries()) {
1259
+ if (value) data[key] = value;
1260
+ }
1261
+
1262
+ if (Object.keys(data).length < 2) {
1263
+ showMessage('Please select at least 2 entities to connect', true);
1264
+ return;
1265
+ }
1266
+
1267
+ try {
1268
+ const response = await fetch(basePath + '/graph/edges', {
1269
+ method: 'POST',
1270
+ headers: { 'Content-Type': 'application/json' },
1271
+ body: JSON.stringify({ edge: data })
1272
+ });
1273
+ const result = await response.json();
1274
+
1275
+ if (result.success) {
1276
+ showMessage('Edge added successfully');
1277
+ e.target.reset();
1278
+ loadGraph();
1279
+ } else {
1280
+ showMessage(result.error, true);
1281
+ }
1282
+ } catch (error) {
1283
+ showMessage('Error: ' + error.message, true);
1284
+ }
1285
+ });
1286
+
1287
+ // Compile authorizations
1288
+ async function compileAuthorizations() {
1289
+ if (!confirm('This will recompile all authorization paths. Continue?')) return;
1290
+
1291
+ try {
1292
+ const response = await fetch(basePath + '/graph/compile_authorizations', {
1293
+ method: 'POST',
1294
+ headers: { 'Content-Type': 'application/json' }
1295
+ });
1296
+ const result = await response.json();
1297
+
1298
+ if (result.success) {
1299
+ showMessage(result.message);
1300
+ } else {
1301
+ showMessage(result.error, true);
1302
+ }
1303
+ } catch (error) {
1304
+ showMessage('Error: ' + error.message, true);
1305
+ }
1306
+ }
1307
+
1308
+ // Load orphans
1309
+ async function loadOrphans() {
1310
+ try {
1311
+ const response = await fetch(basePath + '/graph/orphaned');
1312
+ const data = await response.json();
1313
+ const orphans = data.orphans;
1314
+
1315
+ // Update counts
1316
+ document.getElementById('orphan-users-count').textContent = orphans.users.length;
1317
+ document.getElementById('orphan-groups-count').textContent = orphans.groups.length;
1318
+ document.getElementById('orphan-roles-count').textContent = orphans.roles.length;
1319
+ document.getElementById('orphan-permissions-count').textContent = orphans.permissions.length;
1320
+ document.getElementById('orphan-resources-count').textContent = orphans.resources.length;
1321
+
1322
+ // Update lists
1323
+ const orphanUsersList = document.getElementById('orphan-users-list');
1324
+ orphanUsersList.innerHTML = '';
1325
+ if (orphans.users.length === 0) {
1326
+ orphanUsersList.innerHTML = '<div style="color: #666; padding: 10px;">No orphaned users</div>';
1327
+ } else {
1328
+ orphans.users.forEach(u => {
1329
+ orphanUsersList.innerHTML += `
1330
+ <div class="orphan-item">
1331
+ <div class="name">${u.name}</div>
1332
+ <div class="reason">${u.reason}</div>
1333
+ </div>
1334
+ `;
1335
+ });
1336
+ }
1337
+
1338
+ const orphanGroupsList = document.getElementById('orphan-groups-list');
1339
+ orphanGroupsList.innerHTML = '';
1340
+ if (orphans.groups.length === 0) {
1341
+ orphanGroupsList.innerHTML = '<div style="color: #666; padding: 10px;">No orphaned groups</div>';
1342
+ } else {
1343
+ orphans.groups.forEach(g => {
1344
+ orphanGroupsList.innerHTML += `
1345
+ <div class="orphan-item">
1346
+ <div class="name">${g.name}</div>
1347
+ <div class="reason">${g.reason}</div>
1348
+ </div>
1349
+ `;
1350
+ });
1351
+ }
1352
+
1353
+ const orphanRolesList = document.getElementById('orphan-roles-list');
1354
+ orphanRolesList.innerHTML = '';
1355
+ if (orphans.roles.length === 0) {
1356
+ orphanRolesList.innerHTML = '<div style="color: #666; padding: 10px;">No orphaned roles</div>';
1357
+ } else {
1358
+ orphans.roles.forEach(r => {
1359
+ orphanRolesList.innerHTML += `
1360
+ <div class="orphan-item">
1361
+ <div class="name">${r.name}</div>
1362
+ <div class="reason">${r.reason}</div>
1363
+ </div>
1364
+ `;
1365
+ });
1366
+ }
1367
+
1368
+ const orphanPermissionsList = document.getElementById('orphan-permissions-list');
1369
+ orphanPermissionsList.innerHTML = '';
1370
+ if (orphans.permissions.length === 0) {
1371
+ orphanPermissionsList.innerHTML = '<div style="color: #666; padding: 10px;">No orphaned permissions</div>';
1372
+ } else {
1373
+ orphans.permissions.forEach(p => {
1374
+ orphanPermissionsList.innerHTML += `
1375
+ <div class="orphan-item">
1376
+ <div class="name">${p.name}</div>
1377
+ <div class="reason">${p.reason}</div>
1378
+ </div>
1379
+ `;
1380
+ });
1381
+ }
1382
+
1383
+ const orphanResourcesList = document.getElementById('orphan-resources-list');
1384
+ orphanResourcesList.innerHTML = '';
1385
+ if (orphans.resources.length === 0) {
1386
+ orphanResourcesList.innerHTML = '<div style="color: #666; padding: 10px;">No orphaned resources</div>';
1387
+ } else {
1388
+ orphans.resources.forEach(r => {
1389
+ orphanResourcesList.innerHTML += `
1390
+ <div class="orphan-item">
1391
+ <div class="name">${r.name}</div>
1392
+ <div class="reason">${r.reason}</div>
1393
+ </div>
1394
+ `;
1395
+ });
1396
+ }
1397
+
1398
+ showMessage('Orphans loaded successfully');
1399
+ } catch (error) {
1400
+ showMessage('Error loading orphans: ' + error.message, true);
1401
+ }
1402
+ }
1403
+
1404
+ // Initial load
1405
+ loadGraph();
1406
+ </script>
1407
+ </body>
1408
+ </html>