super_auth 0.1.5 → 0.3.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Gemfile +3 -7
  4. data/Gemfile.lock +25 -14
  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 +1 -0
  14. data/db/migrate/2_groups.rb +8 -1
  15. data/db/migrate/4_roles.rb +8 -1
  16. data/db/migrate/5_resources.rb +1 -0
  17. data/db/migrate/7_authorization.rb +2 -0
  18. data/db/migrate/8_add_indexes_to_edges.rb +17 -0
  19. data/db/migrate/9_add_by_current_user_index.rb +12 -0
  20. data/db/migrate_activerecord/20250101000001_create_super_auth_users.rb +10 -0
  21. data/db/migrate_activerecord/20250101000002_create_super_auth_groups.rb +11 -0
  22. data/db/migrate_activerecord/20250101000003_create_super_auth_permissions.rb +8 -0
  23. data/db/migrate_activerecord/20250101000004_create_super_auth_roles.rb +11 -0
  24. data/db/migrate_activerecord/20250101000005_create_super_auth_resources.rb +10 -0
  25. data/db/migrate_activerecord/20250101000006_create_super_auth_edges.rb +12 -0
  26. data/db/migrate_activerecord/20250101000007_create_super_auth_authorizations.rb +41 -0
  27. data/db/migrate_activerecord/20250101000009_add_by_current_user_index_to_super_auth_authorizations.rb +7 -0
  28. data/db/seeds/sample_data.rb +193 -0
  29. data/lib/basic_loader.rb +0 -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/by_current_user.rb +26 -11
  34. data/lib/super_auth/active_record/edge.rb +45 -0
  35. data/lib/super_auth/active_record/group.rb +7 -0
  36. data/lib/super_auth/active_record/permission.rb +4 -0
  37. data/lib/super_auth/active_record/resource.rb +1 -0
  38. data/lib/super_auth/active_record/role.rb +7 -0
  39. data/lib/super_auth/active_record/user.rb +6 -0
  40. data/lib/super_auth/active_record.rb +17 -0
  41. data/lib/super_auth/edge.rb +190 -131
  42. data/lib/super_auth/group.rb +1 -0
  43. data/lib/super_auth/nestable.rb +17 -10
  44. data/lib/super_auth/permission.rb +1 -1
  45. data/lib/super_auth/railtie.rb +26 -25
  46. data/lib/super_auth/role.rb +2 -1
  47. data/lib/super_auth/user.rb +8 -8
  48. data/lib/super_auth/version.rb +1 -3
  49. data/lib/super_auth.rb +72 -40
  50. data/super_auth.gemspec +35 -0
  51. data/visualization.html +747 -0
  52. metadata +24 -6
@@ -0,0 +1,747 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SuperAuth Graph Visualization</title>
7
+ <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
17
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
+ color: #333;
19
+ height: 100vh;
20
+ overflow: hidden;
21
+ }
22
+
23
+ .container {
24
+ display: grid;
25
+ grid-template-columns: 300px 1fr;
26
+ grid-template-rows: auto 1fr;
27
+ height: 100vh;
28
+ gap: 0;
29
+ }
30
+
31
+ header {
32
+ grid-column: 1 / -1;
33
+ background: rgba(255, 255, 255, 0.95);
34
+ padding: 20px 30px;
35
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
36
+ }
37
+
38
+ h1 {
39
+ font-size: 28px;
40
+ color: #667eea;
41
+ margin-bottom: 5px;
42
+ }
43
+
44
+ .subtitle {
45
+ font-size: 14px;
46
+ color: #666;
47
+ }
48
+
49
+ .sidebar {
50
+ background: rgba(255, 255, 255, 0.95);
51
+ padding: 20px;
52
+ overflow-y: auto;
53
+ box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
54
+ height: 100%;
55
+ }
56
+
57
+ main {
58
+ height: 100%;
59
+ overflow: hidden;
60
+ }
61
+
62
+ .sidebar h2 {
63
+ font-size: 18px;
64
+ color: #667eea;
65
+ margin-bottom: 15px;
66
+ border-bottom: 2px solid #667eea;
67
+ padding-bottom: 5px;
68
+ }
69
+
70
+ .section {
71
+ margin-bottom: 25px;
72
+ }
73
+
74
+ .legend-item {
75
+ display: flex;
76
+ align-items: center;
77
+ margin-bottom: 10px;
78
+ font-size: 13px;
79
+ }
80
+
81
+ .legend-color {
82
+ width: 20px;
83
+ height: 20px;
84
+ border-radius: 50%;
85
+ margin-right: 10px;
86
+ border: 2px solid #fff;
87
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
88
+ }
89
+
90
+ .query-section {
91
+ background: #f8f9fa;
92
+ padding: 15px;
93
+ border-radius: 8px;
94
+ margin-bottom: 20px;
95
+ }
96
+
97
+ select, button {
98
+ width: 100%;
99
+ padding: 10px;
100
+ margin-bottom: 10px;
101
+ border: 1px solid #ddd;
102
+ border-radius: 5px;
103
+ font-size: 13px;
104
+ background: white;
105
+ }
106
+
107
+ button {
108
+ background: #667eea;
109
+ color: white;
110
+ border: none;
111
+ cursor: pointer;
112
+ font-weight: 600;
113
+ transition: background 0.3s;
114
+ }
115
+
116
+ button:hover {
117
+ background: #5568d3;
118
+ }
119
+
120
+ button:disabled {
121
+ background: #ccc;
122
+ cursor: not-allowed;
123
+ }
124
+
125
+ .result {
126
+ margin-top: 10px;
127
+ padding: 10px;
128
+ border-radius: 5px;
129
+ font-size: 12px;
130
+ display: none;
131
+ }
132
+
133
+ .result.success {
134
+ background: #d4edda;
135
+ color: #155724;
136
+ border: 1px solid #c3e6cb;
137
+ display: block;
138
+ }
139
+
140
+ .result.error {
141
+ background: #f8d7da;
142
+ color: #721c24;
143
+ border: 1px solid #f5c6cb;
144
+ display: block;
145
+ }
146
+
147
+ .paths-list {
148
+ max-height: 200px;
149
+ overflow-y: auto;
150
+ font-size: 11px;
151
+ line-height: 1.6;
152
+ }
153
+
154
+ .path-item {
155
+ background: white;
156
+ padding: 8px;
157
+ margin: 5px 0;
158
+ border-radius: 4px;
159
+ border-left: 3px solid #667eea;
160
+ }
161
+
162
+ #network {
163
+ background: rgba(255, 255, 255, 0.98);
164
+ border-radius: 0;
165
+ width: 100%;
166
+ height: 100%;
167
+ }
168
+
169
+ .stats {
170
+ font-size: 12px;
171
+ color: #666;
172
+ margin-top: 10px;
173
+ }
174
+
175
+ .stats-item {
176
+ display: flex;
177
+ justify-content: space-between;
178
+ margin-bottom: 5px;
179
+ padding: 5px;
180
+ background: #f8f9fa;
181
+ border-radius: 3px;
182
+ }
183
+
184
+ .stats-label {
185
+ font-weight: 600;
186
+ }
187
+
188
+ .example-buttons {
189
+ display: flex;
190
+ flex-direction: column;
191
+ gap: 8px;
192
+ }
193
+
194
+ .example-buttons button {
195
+ font-size: 12px;
196
+ padding: 8px;
197
+ }
198
+
199
+ .info-box {
200
+ background: #e3f2fd;
201
+ padding: 12px;
202
+ border-radius: 5px;
203
+ font-size: 12px;
204
+ margin-top: 15px;
205
+ border-left: 3px solid #2196f3;
206
+ }
207
+
208
+ .info-box strong {
209
+ color: #1976d2;
210
+ }
211
+
212
+ .controls {
213
+ margin-top: 20px;
214
+ }
215
+
216
+ .control-group {
217
+ margin-bottom: 15px;
218
+ }
219
+
220
+ .control-label {
221
+ font-size: 12px;
222
+ font-weight: 600;
223
+ margin-bottom: 5px;
224
+ color: #555;
225
+ }
226
+
227
+ .node-info {
228
+ background: white;
229
+ padding: 10px;
230
+ border-radius: 5px;
231
+ margin-top: 10px;
232
+ font-size: 12px;
233
+ display: none;
234
+ }
235
+
236
+ .node-info.visible {
237
+ display: block;
238
+ }
239
+ </style>
240
+ </head>
241
+ <body>
242
+ <div class="container">
243
+ <header>
244
+ <h1>SuperAuth Graph Visualization</h1>
245
+ <p class="subtitle">Interactive visualization of the authorization graph - Click nodes to explore connections</p>
246
+ </header>
247
+
248
+ <aside class="sidebar">
249
+ <div class="section query-section">
250
+ <h2>Authorization Query</h2>
251
+ <select id="userSelect">
252
+ <option value="">Select User...</option>
253
+ </select>
254
+ <select id="resourceSelect">
255
+ <option value="">Select Resource...</option>
256
+ </select>
257
+ <button id="queryBtn">Find Authorization Paths</button>
258
+ <div id="result" class="result"></div>
259
+ </div>
260
+
261
+ <div class="section">
262
+ <h2>Legend</h2>
263
+ <div class="legend-item">
264
+ <div class="legend-color" style="background: #4A90E2;"></div>
265
+ <span>Users</span>
266
+ </div>
267
+ <div class="legend-item">
268
+ <div class="legend-color" style="background: #7ED321;"></div>
269
+ <span>Groups (hierarchical)</span>
270
+ </div>
271
+ <div class="legend-item">
272
+ <div class="legend-color" style="background: #F5A623;"></div>
273
+ <span>Roles (hierarchical)</span>
274
+ </div>
275
+ <div class="legend-item">
276
+ <div class="legend-color" style="background: #BD10E0;"></div>
277
+ <span>Permissions</span>
278
+ </div>
279
+ <div class="legend-item">
280
+ <div class="legend-color" style="background: #E74C3C;"></div>
281
+ <span>Resources</span>
282
+ </div>
283
+ </div>
284
+
285
+ <div class="section">
286
+ <h2>Quick Examples</h2>
287
+ <div class="example-buttons">
288
+ <button onclick="loadExample1()">Peter → core_design_template</button>
289
+ <button onclick="loadExample2()">Michael → app1 (deploy)</button>
290
+ <button onclick="loadExample3()">Anna → customer_posts</button>
291
+ <button onclick="clearHighlights()">Clear Highlights</button>
292
+ </div>
293
+ </div>
294
+
295
+ <div class="section">
296
+ <h2>Statistics</h2>
297
+ <div class="stats">
298
+ <div class="stats-item">
299
+ <span class="stats-label">Users:</span>
300
+ <span id="statUsers">0</span>
301
+ </div>
302
+ <div class="stats-item">
303
+ <span class="stats-label">Groups:</span>
304
+ <span id="statGroups">0</span>
305
+ </div>
306
+ <div class="stats-item">
307
+ <span class="stats-label">Roles:</span>
308
+ <span id="statRoles">0</span>
309
+ </div>
310
+ <div class="stats-item">
311
+ <span class="stats-label">Permissions:</span>
312
+ <span id="statPerms">0</span>
313
+ </div>
314
+ <div class="stats-item">
315
+ <span class="stats-label">Resources:</span>
316
+ <span id="statResources">0</span>
317
+ </div>
318
+ <div class="stats-item">
319
+ <span class="stats-label">Edges:</span>
320
+ <span id="statEdges">0</span>
321
+ </div>
322
+ </div>
323
+ </div>
324
+
325
+ <div class="node-info" id="nodeInfo">
326
+ <strong>Selected:</strong> <span id="nodeName"></span><br>
327
+ <span id="nodeDetails"></span>
328
+ </div>
329
+
330
+ <div class="info-box">
331
+ <strong>How it works:</strong><br>
332
+ SuperAuth finds paths from Users to Resources through Groups, Roles, and Permissions.
333
+ If any path exists, access is granted!
334
+ </div>
335
+ </aside>
336
+
337
+ <main>
338
+ <div id="network"></div>
339
+ </main>
340
+ </div>
341
+
342
+ <script>
343
+ // Global variables for data
344
+ let data = { users: [], groups: [], roles: [], permissions: [], resources: [] };
345
+ let edges = [];
346
+ let network = null;
347
+
348
+ // Prepare nodes for vis.js
349
+ const nodes = new vis.DataSet();
350
+ const colorMap = {
351
+ user: '#4A90E2',
352
+ group: '#7ED321',
353
+ role: '#F5A623',
354
+ permission: '#BD10E0',
355
+ resource: '#E74C3C'
356
+ };
357
+
358
+ const edgesDataSet = new vis.DataSet();
359
+
360
+ // API base URL - can be configured
361
+ const API_BASE = '/super_auth';
362
+
363
+ // Fetch data from backend
364
+ async function loadGraphData() {
365
+ try {
366
+ const response = await fetch(`${API_BASE}/graph`);
367
+ const graphData = await response.json();
368
+
369
+ // Process nodes
370
+ graphData.nodes.forEach(node => {
371
+ const entityType = node.type;
372
+
373
+ // Group nodes by type
374
+ if (entityType === 'user') data.users.push(node);
375
+ else if (entityType === 'group') data.groups.push(node);
376
+ else if (entityType === 'role') data.roles.push(node);
377
+ else if (entityType === 'permission') data.permissions.push(node);
378
+ else if (entityType === 'resource') data.resources.push(node);
379
+
380
+ // Add to vis.js nodes
381
+ nodes.add({
382
+ id: node.id,
383
+ label: node.label,
384
+ color: {
385
+ background: colorMap[node.type],
386
+ border: colorMap[node.type],
387
+ highlight: {
388
+ background: colorMap[node.type],
389
+ border: '#000'
390
+ }
391
+ },
392
+ shape: 'circle',
393
+ size: node.type === 'user' || node.type === 'resource' ? 25 : 20,
394
+ font: {
395
+ size: 12,
396
+ color: '#333',
397
+ face: 'Segoe UI'
398
+ },
399
+ borderWidth: 2,
400
+ type: node.type,
401
+ database_id: node.database_id
402
+ });
403
+ });
404
+
405
+ // Process edges
406
+ graphData.edges.forEach((edge, i) => {
407
+ const isDashes = edge.type === 'hierarchy';
408
+ const edgeData = {
409
+ id: i,
410
+ from: edge.from,
411
+ to: edge.to,
412
+ arrows: '',
413
+ color: isDashes ? { color: '#ccc' } : { color: '#848484' },
414
+ width: isDashes ? 1 : 2,
415
+ dashes: isDashes,
416
+ smooth: { type: 'continuous' }
417
+ };
418
+
419
+ edges.push(edge);
420
+ edgesDataSet.add(edgeData);
421
+ });
422
+
423
+ // Update stats
424
+ updateStats(graphData.stats);
425
+
426
+ // Initialize network
427
+ initializeNetwork();
428
+
429
+ // Populate dropdowns
430
+ populateDropdowns();
431
+
432
+ } catch (error) {
433
+ console.error('Error loading graph data:', error);
434
+ alert('Failed to load graph data. Make sure the Rails backend is running and the SuperAuth engine is mounted.');
435
+ }
436
+ }
437
+
438
+ // Initialize the network visualization
439
+ function initializeNetwork() {
440
+ const container = document.getElementById('network');
441
+ const networkData = { nodes: nodes, edges: edgesDataSet };
442
+ const options = {
443
+ layout: {
444
+ hierarchical: false
445
+ },
446
+ physics: {
447
+ enabled: true,
448
+ barnesHut: {
449
+ gravitationalConstant: -3000,
450
+ centralGravity: 0.3,
451
+ springLength: 150,
452
+ springConstant: 0.04
453
+ },
454
+ stabilization: {
455
+ iterations: 200
456
+ }
457
+ },
458
+ interaction: {
459
+ hover: true,
460
+ tooltipDelay: 200,
461
+ zoomView: true,
462
+ dragView: true
463
+ },
464
+ nodes: {
465
+ shadow: {
466
+ enabled: true,
467
+ color: 'rgba(0,0,0,0.2)',
468
+ size: 5,
469
+ x: 2,
470
+ y: 2
471
+ }
472
+ }
473
+ };
474
+
475
+ network = new vis.Network(container, networkData, options);
476
+
477
+ // Setup event handlers
478
+ network.on('selectNode', (params) => {
479
+ const nodeId = params.nodes[0];
480
+ const node = nodes.get(nodeId);
481
+ const nodeInfo = document.getElementById('nodeInfo');
482
+ const nodeName = document.getElementById('nodeName');
483
+ const nodeDetails = document.getElementById('nodeDetails');
484
+
485
+ nodeName.textContent = node.label;
486
+ nodeDetails.innerHTML = `Type: ${node.type}<br>ID: ${nodeId}`;
487
+ nodeInfo.classList.add('visible');
488
+ });
489
+
490
+ network.on('deselectNode', () => {
491
+ document.getElementById('nodeInfo').classList.remove('visible');
492
+ });
493
+
494
+ // Stabilize network
495
+ network.once('stabilizationIterationsDone', () => {
496
+ network.setOptions({ physics: false });
497
+ });
498
+ }
499
+
500
+ // Populate dropdowns
501
+ function populateDropdowns() {
502
+ const userSelect = document.getElementById('userSelect');
503
+ const resourceSelect = document.getElementById('resourceSelect');
504
+
505
+ // Clear existing options (except first placeholder)
506
+ userSelect.innerHTML = '<option value="">Select User...</option>';
507
+ resourceSelect.innerHTML = '<option value="">Select Resource...</option>';
508
+
509
+ data.users.forEach(user => {
510
+ const option = document.createElement('option');
511
+ option.value = user.database_id;
512
+ option.setAttribute('data-node-id', user.id);
513
+ option.textContent = user.label;
514
+ userSelect.appendChild(option);
515
+ });
516
+
517
+ data.resources.forEach(resource => {
518
+ const option = document.createElement('option');
519
+ option.value = resource.database_id;
520
+ option.setAttribute('data-node-id', resource.id);
521
+ option.textContent = resource.label;
522
+ resourceSelect.appendChild(option);
523
+ });
524
+ }
525
+
526
+ // Update statistics
527
+ function updateStats(stats) {
528
+ if (stats) {
529
+ document.getElementById('statUsers').textContent = stats.users;
530
+ document.getElementById('statGroups').textContent = stats.groups;
531
+ document.getElementById('statRoles').textContent = stats.roles;
532
+ document.getElementById('statPerms').textContent = stats.permissions;
533
+ document.getElementById('statResources').textContent = stats.resources;
534
+ document.getElementById('statEdges').textContent = stats.edges;
535
+ } else {
536
+ document.getElementById('statUsers').textContent = data.users.length;
537
+ document.getElementById('statGroups').textContent = data.groups.length;
538
+ document.getElementById('statRoles').textContent = data.roles.length;
539
+ document.getElementById('statPerms').textContent = data.permissions.length;
540
+ document.getElementById('statResources').textContent = data.resources.length;
541
+ document.getElementById('statEdges').textContent = edges.length;
542
+ }
543
+ }
544
+
545
+ // BFS to find all paths
546
+ function findAllPaths(startId, endId) {
547
+ const paths = [];
548
+ const queue = [[startId]];
549
+ const visited = new Set();
550
+
551
+ while (queue.length > 0) {
552
+ const path = queue.shift();
553
+ const node = path[path.length - 1];
554
+
555
+ if (node === endId) {
556
+ paths.push(path);
557
+ continue;
558
+ }
559
+
560
+ const pathKey = path.join(',');
561
+ if (visited.has(pathKey)) continue;
562
+ visited.add(pathKey);
563
+
564
+ // Find all neighbors
565
+ edges.forEach(edge => {
566
+ if (edge.from === node && !path.includes(edge.to)) {
567
+ queue.push([...path, edge.to]);
568
+ }
569
+ if (edge.to === node && !path.includes(edge.from)) {
570
+ queue.push([...path, edge.from]);
571
+ }
572
+ });
573
+
574
+ // Limit search depth
575
+ if (path.length > 10) continue;
576
+ }
577
+
578
+ return paths;
579
+ }
580
+
581
+ // Get label for node ID
582
+ function getNodeLabel(id) {
583
+ const allItems = [...data.users, ...data.groups, ...data.roles, ...data.permissions, ...data.resources];
584
+ const item = allItems.find(i => i.id === id);
585
+ return item ? item.label : id;
586
+ }
587
+
588
+ // Query authorization
589
+ document.getElementById('queryBtn').addEventListener('click', async () => {
590
+ const userSelect = document.getElementById('userSelect');
591
+ const resourceSelect = document.getElementById('resourceSelect');
592
+ const userId = userSelect.value;
593
+ const resourceId = resourceSelect.value;
594
+ const resultDiv = document.getElementById('result');
595
+
596
+ if (!userId || !resourceId) {
597
+ resultDiv.className = 'result error';
598
+ resultDiv.innerHTML = 'Please select both a user and a resource.';
599
+ return;
600
+ }
601
+
602
+ // Get node IDs for graph highlighting
603
+ const userNodeId = userSelect.options[userSelect.selectedIndex].getAttribute('data-node-id');
604
+ const resourceNodeId = resourceSelect.options[resourceSelect.selectedIndex].getAttribute('data-node-id');
605
+
606
+ clearHighlights();
607
+
608
+ try {
609
+ // Call backend API
610
+ const response = await fetch(`${API_BASE}/graph/authorize?user_id=${userId}&resource_id=${resourceId}`);
611
+ const result = await response.json();
612
+
613
+ if (result.authorized) {
614
+ resultDiv.className = 'result success';
615
+ resultDiv.innerHTML = `<strong>Access Granted!</strong> Found ${result.count} authorization path(s):<br>`;
616
+
617
+ const pathsList = document.createElement('div');
618
+ pathsList.className = 'paths-list';
619
+
620
+ result.paths.forEach((path, i) => {
621
+ const pathItem = document.createElement('div');
622
+ pathItem.className = 'path-item';
623
+ pathItem.innerHTML = `Path ${i + 1}: ${path.join(' → ')}`;
624
+ pathsList.appendChild(pathItem);
625
+ });
626
+
627
+ resultDiv.appendChild(pathsList);
628
+
629
+ // Highlight path on graph using local node IDs
630
+ if (userNodeId && resourceNodeId) {
631
+ const localPath = findAllPaths(userNodeId, resourceNodeId);
632
+ if (localPath.length > 0) {
633
+ highlightPath(localPath[0]);
634
+ }
635
+ }
636
+ } else {
637
+ resultDiv.className = 'result error';
638
+ resultDiv.innerHTML = '<strong>Access Denied!</strong> No authorization path found.';
639
+ }
640
+ } catch (error) {
641
+ console.error('Error querying authorization:', error);
642
+ resultDiv.className = 'result error';
643
+ resultDiv.innerHTML = '<strong>Error:</strong> Failed to query authorization. Check console for details.';
644
+ }
645
+ });
646
+
647
+ // Highlight a path
648
+ function highlightPath(path) {
649
+ // Highlight nodes
650
+ path.forEach(nodeId => {
651
+ nodes.update({
652
+ id: nodeId,
653
+ borderWidth: 4,
654
+ shadow: {
655
+ enabled: true,
656
+ color: 'rgba(255, 215, 0, 0.6)',
657
+ size: 15,
658
+ x: 0,
659
+ y: 0
660
+ }
661
+ });
662
+ });
663
+
664
+ // Highlight edges
665
+ for (let i = 0; i < path.length - 1; i++) {
666
+ const from = path[i];
667
+ const to = path[i + 1];
668
+
669
+ edgesDataSet.forEach(edge => {
670
+ if ((edge.from === from && edge.to === to) || (edge.from === to && edge.to === from)) {
671
+ edgesDataSet.update({
672
+ id: edge.id,
673
+ width: 4,
674
+ color: { color: '#FFD700' }
675
+ });
676
+ }
677
+ });
678
+ }
679
+
680
+ // Focus on path
681
+ network.fit({ nodes: path, animation: true });
682
+ }
683
+
684
+ // Clear highlights
685
+ function clearHighlights() {
686
+ nodes.forEach(node => {
687
+ nodes.update({
688
+ id: node.id,
689
+ borderWidth: 2,
690
+ shadow: {
691
+ enabled: true,
692
+ color: 'rgba(0,0,0,0.2)',
693
+ size: 5,
694
+ x: 2,
695
+ y: 2
696
+ }
697
+ });
698
+ });
699
+
700
+ edges.forEach((e, i) => {
701
+ edgesDataSet.update({
702
+ id: i,
703
+ width: e.dashes ? 1 : 2,
704
+ color: e.color || { color: '#848484' }
705
+ });
706
+ });
707
+ }
708
+
709
+ // Example functions
710
+ function loadExample1() {
711
+ // Peter -> core_design_template
712
+ const peter = data.users.find(u => u.label === 'Peter');
713
+ const coreDesign = data.resources.find(r => r.label === 'core_design_template');
714
+ if (peter && coreDesign) {
715
+ document.getElementById('userSelect').value = peter.database_id;
716
+ document.getElementById('resourceSelect').value = coreDesign.database_id;
717
+ document.getElementById('queryBtn').click();
718
+ }
719
+ }
720
+
721
+ function loadExample2() {
722
+ // Michael -> app1
723
+ const michael = data.users.find(u => u.label === 'Michael');
724
+ const app1 = data.resources.find(r => r.label === 'app1');
725
+ if (michael && app1) {
726
+ document.getElementById('userSelect').value = michael.database_id;
727
+ document.getElementById('resourceSelect').value = app1.database_id;
728
+ document.getElementById('queryBtn').click();
729
+ }
730
+ }
731
+
732
+ function loadExample3() {
733
+ // Anna -> customer_post1
734
+ const anna = data.users.find(u => u.label === 'Anna');
735
+ const post1 = data.resources.find(r => r.label === 'customer_post1');
736
+ if (anna && post1) {
737
+ document.getElementById('userSelect').value = anna.database_id;
738
+ document.getElementById('resourceSelect').value = post1.database_id;
739
+ document.getElementById('queryBtn').click();
740
+ }
741
+ }
742
+
743
+ // Initialize - load data from API
744
+ loadGraphData();
745
+ </script>
746
+ </body>
747
+ </html>