decision_agent 0.1.3 → 0.1.6

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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -233
  3. data/lib/decision_agent/ab_testing/ab_test.rb +197 -0
  4. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +76 -0
  5. data/lib/decision_agent/ab_testing/ab_test_manager.rb +317 -0
  6. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +188 -0
  7. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +155 -0
  8. data/lib/decision_agent/ab_testing/storage/adapter.rb +67 -0
  9. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +116 -0
  10. data/lib/decision_agent/agent.rb +5 -3
  11. data/lib/decision_agent/auth/access_audit_logger.rb +122 -0
  12. data/lib/decision_agent/auth/authenticator.rb +127 -0
  13. data/lib/decision_agent/auth/password_reset_manager.rb +57 -0
  14. data/lib/decision_agent/auth/password_reset_token.rb +33 -0
  15. data/lib/decision_agent/auth/permission.rb +29 -0
  16. data/lib/decision_agent/auth/permission_checker.rb +43 -0
  17. data/lib/decision_agent/auth/rbac_adapter.rb +278 -0
  18. data/lib/decision_agent/auth/rbac_config.rb +51 -0
  19. data/lib/decision_agent/auth/role.rb +56 -0
  20. data/lib/decision_agent/auth/session.rb +33 -0
  21. data/lib/decision_agent/auth/session_manager.rb +57 -0
  22. data/lib/decision_agent/auth/user.rb +70 -0
  23. data/lib/decision_agent/context.rb +24 -4
  24. data/lib/decision_agent/decision.rb +10 -3
  25. data/lib/decision_agent/dsl/condition_evaluator.rb +378 -1
  26. data/lib/decision_agent/dsl/schema_validator.rb +8 -1
  27. data/lib/decision_agent/errors.rb +38 -0
  28. data/lib/decision_agent/evaluation.rb +10 -3
  29. data/lib/decision_agent/evaluation_validator.rb +8 -13
  30. data/lib/decision_agent/monitoring/dashboard_server.rb +1 -0
  31. data/lib/decision_agent/monitoring/metrics_collector.rb +164 -7
  32. data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +253 -0
  33. data/lib/decision_agent/monitoring/storage/base_adapter.rb +90 -0
  34. data/lib/decision_agent/monitoring/storage/memory_adapter.rb +222 -0
  35. data/lib/decision_agent/testing/batch_test_importer.rb +373 -0
  36. data/lib/decision_agent/testing/batch_test_runner.rb +244 -0
  37. data/lib/decision_agent/testing/test_coverage_analyzer.rb +191 -0
  38. data/lib/decision_agent/testing/test_result_comparator.rb +235 -0
  39. data/lib/decision_agent/testing/test_scenario.rb +42 -0
  40. data/lib/decision_agent/version.rb +10 -1
  41. data/lib/decision_agent/versioning/activerecord_adapter.rb +1 -1
  42. data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -28
  43. data/lib/decision_agent/web/middleware/auth_middleware.rb +45 -0
  44. data/lib/decision_agent/web/middleware/permission_middleware.rb +94 -0
  45. data/lib/decision_agent/web/public/app.js +184 -29
  46. data/lib/decision_agent/web/public/batch_testing.html +640 -0
  47. data/lib/decision_agent/web/public/index.html +37 -9
  48. data/lib/decision_agent/web/public/login.html +298 -0
  49. data/lib/decision_agent/web/public/users.html +679 -0
  50. data/lib/decision_agent/web/server.rb +873 -7
  51. data/lib/decision_agent.rb +59 -0
  52. data/lib/generators/decision_agent/install/install_generator.rb +37 -0
  53. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +45 -0
  54. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +54 -0
  55. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +43 -0
  56. data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +189 -0
  57. data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +114 -0
  58. data/lib/generators/decision_agent/install/templates/decision_log.rb +57 -0
  59. data/lib/generators/decision_agent/install/templates/error_metric.rb +53 -0
  60. data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +43 -0
  61. data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +109 -0
  62. data/lib/generators/decision_agent/install/templates/performance_metric.rb +76 -0
  63. data/lib/generators/decision_agent/install/templates/rule_version.rb +1 -1
  64. data/spec/ab_testing/ab_test_assignment_spec.rb +253 -0
  65. data/spec/ab_testing/ab_test_manager_spec.rb +612 -0
  66. data/spec/ab_testing/ab_test_spec.rb +270 -0
  67. data/spec/ab_testing/ab_testing_agent_spec.rb +481 -0
  68. data/spec/ab_testing/storage/adapter_spec.rb +64 -0
  69. data/spec/ab_testing/storage/memory_adapter_spec.rb +485 -0
  70. data/spec/advanced_operators_spec.rb +1003 -0
  71. data/spec/agent_spec.rb +40 -0
  72. data/spec/audit_adapters_spec.rb +18 -0
  73. data/spec/auth/access_audit_logger_spec.rb +394 -0
  74. data/spec/auth/authenticator_spec.rb +112 -0
  75. data/spec/auth/password_reset_spec.rb +294 -0
  76. data/spec/auth/permission_checker_spec.rb +207 -0
  77. data/spec/auth/permission_spec.rb +73 -0
  78. data/spec/auth/rbac_adapter_spec.rb +550 -0
  79. data/spec/auth/rbac_config_spec.rb +82 -0
  80. data/spec/auth/role_spec.rb +51 -0
  81. data/spec/auth/session_manager_spec.rb +172 -0
  82. data/spec/auth/session_spec.rb +112 -0
  83. data/spec/auth/user_spec.rb +130 -0
  84. data/spec/context_spec.rb +43 -0
  85. data/spec/decision_agent_spec.rb +96 -0
  86. data/spec/decision_spec.rb +423 -0
  87. data/spec/dsl/condition_evaluator_spec.rb +774 -0
  88. data/spec/evaluation_spec.rb +364 -0
  89. data/spec/evaluation_validator_spec.rb +165 -0
  90. data/spec/examples.txt +1542 -548
  91. data/spec/issue_verification_spec.rb +95 -21
  92. data/spec/monitoring/metrics_collector_spec.rb +221 -3
  93. data/spec/monitoring/monitored_agent_spec.rb +1 -1
  94. data/spec/monitoring/prometheus_exporter_spec.rb +1 -1
  95. data/spec/monitoring/storage/activerecord_adapter_spec.rb +498 -0
  96. data/spec/monitoring/storage/base_adapter_spec.rb +61 -0
  97. data/spec/monitoring/storage/memory_adapter_spec.rb +247 -0
  98. data/spec/performance_optimizations_spec.rb +486 -0
  99. data/spec/spec_helper.rb +23 -0
  100. data/spec/testing/batch_test_importer_spec.rb +693 -0
  101. data/spec/testing/batch_test_runner_spec.rb +307 -0
  102. data/spec/testing/test_coverage_analyzer_spec.rb +292 -0
  103. data/spec/testing/test_result_comparator_spec.rb +392 -0
  104. data/spec/testing/test_scenario_spec.rb +113 -0
  105. data/spec/versioning/adapter_spec.rb +156 -0
  106. data/spec/versioning_spec.rb +253 -0
  107. data/spec/web/middleware/auth_middleware_spec.rb +133 -0
  108. data/spec/web/middleware/permission_middleware_spec.rb +247 -0
  109. data/spec/web_ui_rack_spec.rb +1705 -0
  110. metadata +123 -6
@@ -0,0 +1,679 @@
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>User Management - DecisionAgent</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+ <style>
9
+ .users-container {
10
+ max-width: 1200px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ }
14
+
15
+ .page-header {
16
+ display: flex;
17
+ justify-content: space-between;
18
+ align-items: center;
19
+ margin-bottom: 30px;
20
+ padding: 20px;
21
+ background: var(--panel-bg);
22
+ border-radius: 10px;
23
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
24
+ }
25
+
26
+ .page-header h1 {
27
+ font-size: 2rem;
28
+ color: var(--primary-color);
29
+ }
30
+
31
+ .header-actions {
32
+ display: flex;
33
+ gap: 10px;
34
+ align-items: center;
35
+ }
36
+
37
+ .user-info {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 15px;
41
+ padding: 10px 15px;
42
+ background: var(--hover-bg);
43
+ border-radius: 6px;
44
+ }
45
+
46
+ .user-info span {
47
+ color: var(--text-secondary);
48
+ }
49
+
50
+ .users-panel {
51
+ background: var(--panel-bg);
52
+ border-radius: 10px;
53
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
54
+ overflow: hidden;
55
+ }
56
+
57
+ .panel-header {
58
+ padding: 20px;
59
+ border-bottom: 2px solid var(--border-color);
60
+ display: flex;
61
+ justify-content: space-between;
62
+ align-items: center;
63
+ background: var(--hover-bg);
64
+ }
65
+
66
+ .users-table {
67
+ width: 100%;
68
+ border-collapse: collapse;
69
+ }
70
+
71
+ .users-table th,
72
+ .users-table td {
73
+ padding: 15px;
74
+ text-align: left;
75
+ border-bottom: 1px solid var(--border-color);
76
+ }
77
+
78
+ .users-table th {
79
+ background: var(--hover-bg);
80
+ font-weight: 600;
81
+ color: var(--text-color);
82
+ }
83
+
84
+ .users-table tr:hover {
85
+ background: var(--hover-bg);
86
+ }
87
+
88
+ .role-badge {
89
+ display: inline-block;
90
+ padding: 4px 12px;
91
+ border-radius: 12px;
92
+ font-size: 0.875rem;
93
+ font-weight: 500;
94
+ margin: 2px;
95
+ }
96
+
97
+ .role-admin { background: #fee2e2; color: #991b1b; }
98
+ .role-editor { background: #dbeafe; color: #1e40af; }
99
+ .role-viewer { background: #e0e7ff; color: #3730a3; }
100
+ .role-auditor { background: #fef3c7; color: #92400e; }
101
+ .role-approver { background: #d1fae5; color: #065f46; }
102
+
103
+ .status-badge {
104
+ display: inline-block;
105
+ padding: 4px 12px;
106
+ border-radius: 12px;
107
+ font-size: 0.875rem;
108
+ font-weight: 500;
109
+ }
110
+
111
+ .status-active { background: #d1fae5; color: #065f46; }
112
+ .status-inactive { background: #fee2e2; color: #991b1b; }
113
+
114
+ .action-buttons {
115
+ display: flex;
116
+ gap: 8px;
117
+ }
118
+
119
+ .btn-icon {
120
+ padding: 6px 12px;
121
+ border: none;
122
+ border-radius: 4px;
123
+ cursor: pointer;
124
+ font-size: 0.875rem;
125
+ transition: all 0.2s;
126
+ }
127
+
128
+ .btn-icon:hover {
129
+ transform: translateY(-1px);
130
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
131
+ }
132
+
133
+ .btn-edit {
134
+ background: #dbeafe;
135
+ color: #1e40af;
136
+ }
137
+
138
+ .btn-delete {
139
+ background: #fee2e2;
140
+ color: #991b1b;
141
+ }
142
+
143
+ .modal {
144
+ display: none;
145
+ position: fixed;
146
+ top: 0;
147
+ left: 0;
148
+ width: 100%;
149
+ height: 100%;
150
+ background: rgba(0, 0, 0, 0.5);
151
+ z-index: 1000;
152
+ align-items: center;
153
+ justify-content: center;
154
+ }
155
+
156
+ .modal.show {
157
+ display: flex;
158
+ }
159
+
160
+ .modal-content {
161
+ background: var(--panel-bg);
162
+ border-radius: 10px;
163
+ padding: 30px;
164
+ max-width: 500px;
165
+ width: 90%;
166
+ max-height: 90vh;
167
+ overflow-y: auto;
168
+ }
169
+
170
+ .modal-header {
171
+ display: flex;
172
+ justify-content: space-between;
173
+ align-items: center;
174
+ margin-bottom: 20px;
175
+ }
176
+
177
+ .modal-header h2 {
178
+ font-size: 1.5rem;
179
+ color: var(--text-color);
180
+ }
181
+
182
+ .close-btn {
183
+ background: none;
184
+ border: none;
185
+ font-size: 1.5rem;
186
+ cursor: pointer;
187
+ color: var(--text-secondary);
188
+ }
189
+
190
+ .form-group {
191
+ margin-bottom: 20px;
192
+ }
193
+
194
+ .form-group label {
195
+ display: block;
196
+ margin-bottom: 8px;
197
+ font-weight: 500;
198
+ color: var(--text-color);
199
+ }
200
+
201
+ .form-group input,
202
+ .form-group select {
203
+ width: 100%;
204
+ padding: 10px;
205
+ border: 2px solid var(--border-color);
206
+ border-radius: 6px;
207
+ font-size: 1rem;
208
+ }
209
+
210
+ .form-group input:focus,
211
+ .form-group select:focus {
212
+ outline: none;
213
+ border-color: var(--primary-color);
214
+ }
215
+
216
+ .checkbox-group {
217
+ display: flex;
218
+ flex-wrap: wrap;
219
+ gap: 10px;
220
+ margin-top: 10px;
221
+ }
222
+
223
+ .checkbox-item {
224
+ display: flex;
225
+ align-items: center;
226
+ gap: 5px;
227
+ }
228
+
229
+ .checkbox-item input[type="checkbox"] {
230
+ width: auto;
231
+ }
232
+
233
+ .modal-actions {
234
+ display: flex;
235
+ gap: 10px;
236
+ justify-content: flex-end;
237
+ margin-top: 20px;
238
+ }
239
+
240
+ .alert {
241
+ padding: 12px;
242
+ border-radius: 6px;
243
+ margin-bottom: 20px;
244
+ display: none;
245
+ }
246
+
247
+ .alert.show {
248
+ display: block;
249
+ }
250
+
251
+ .alert-error {
252
+ background: #fee2e2;
253
+ color: #991b1b;
254
+ }
255
+
256
+ .alert-success {
257
+ background: #d1fae5;
258
+ color: #065f46;
259
+ }
260
+
261
+ .empty-state {
262
+ text-align: center;
263
+ padding: 60px 20px;
264
+ color: var(--text-secondary);
265
+ }
266
+ </style>
267
+ </head>
268
+ <body>
269
+ <div class="users-container">
270
+ <div class="page-header">
271
+ <h1>👥 User Management</h1>
272
+ <div class="header-actions">
273
+ <div class="user-info" id="currentUserInfo">
274
+ <span>Loading...</span>
275
+ </div>
276
+ <button class="btn btn-primary" id="addUserBtn">+ Add User</button>
277
+ <button class="btn btn-secondary" onclick="window.location.href='/'">← Home</button>
278
+ </div>
279
+ </div>
280
+
281
+ <div class="users-panel">
282
+ <div class="panel-header">
283
+ <h2>Users</h2>
284
+ <button class="btn btn-secondary" id="refreshBtn">🔄 Refresh</button>
285
+ </div>
286
+
287
+ <div id="alertContainer"></div>
288
+
289
+ <div style="overflow-x: auto;">
290
+ <table class="users-table">
291
+ <thead>
292
+ <tr>
293
+ <th>Email</th>
294
+ <th>Roles</th>
295
+ <th>Status</th>
296
+ <th>Created</th>
297
+ <th>Actions</th>
298
+ </tr>
299
+ </thead>
300
+ <tbody id="usersTableBody">
301
+ <tr>
302
+ <td colspan="5" class="empty-state">Loading users...</td>
303
+ </tr>
304
+ </tbody>
305
+ </table>
306
+ </div>
307
+ </div>
308
+ </div>
309
+
310
+ <!-- Add/Edit User Modal -->
311
+ <div class="modal" id="userModal">
312
+ <div class="modal-content">
313
+ <div class="modal-header">
314
+ <h2 id="modalTitle">Add User</h2>
315
+ <button class="close-btn" id="closeModalBtn">&times;</button>
316
+ </div>
317
+ <form id="userForm">
318
+ <input type="hidden" id="userId" name="userId">
319
+ <div class="form-group">
320
+ <label for="userEmail">Email *</label>
321
+ <input type="email" id="userEmail" name="email" required>
322
+ </div>
323
+ <div class="form-group">
324
+ <label for="userPassword">Password *</label>
325
+ <input type="password" id="userPassword" name="password" required>
326
+ <small style="color: var(--text-secondary);">Minimum 8 characters</small>
327
+ </div>
328
+ <div class="form-group">
329
+ <label>Roles</label>
330
+ <div class="checkbox-group" id="rolesCheckboxGroup">
331
+ <!-- Roles will be populated dynamically -->
332
+ </div>
333
+ </div>
334
+ <div class="modal-actions">
335
+ <button type="button" class="btn btn-secondary" id="cancelBtn">Cancel</button>
336
+ <button type="submit" class="btn btn-primary" id="saveBtn">Save</button>
337
+ </div>
338
+ </form>
339
+ </div>
340
+ </div>
341
+
342
+ <script>
343
+ // Helper function to get the base path for API calls
344
+ function getBasePath() {
345
+ const baseTag = document.querySelector('base');
346
+ if (baseTag && baseTag.href) {
347
+ try {
348
+ const baseUrl = new URL(baseTag.href, window.location.href);
349
+ let path = baseUrl.pathname;
350
+ if (path && !path.endsWith('/')) {
351
+ path += '/';
352
+ }
353
+ return path;
354
+ } catch (e) {
355
+ if (baseTag.href.startsWith('/')) {
356
+ const match = baseTag.href.match(/^(https?:\/\/[^\/]+)?(\/.*?)\/?$/);
357
+ if (match && match[2]) {
358
+ return match[2].endsWith('/') ? match[2] : match[2] + '/';
359
+ }
360
+ } else if (baseTag.href.startsWith('./')) {
361
+ const pathname = window.location.pathname;
362
+ return pathname.substring(0, pathname.lastIndexOf('/') + 1);
363
+ }
364
+ }
365
+ }
366
+ const pathname = window.location.pathname;
367
+ if (pathname.includes('/decision_agent')) {
368
+ const match = pathname.match(/^(\/.*?\/decision_agent)\/?/);
369
+ if (match) {
370
+ return match[1].endsWith('/') ? match[1] : match[1] + '/';
371
+ }
372
+ }
373
+ const dirPath = pathname.substring(0, pathname.lastIndexOf('/') + 1);
374
+ return dirPath || '/';
375
+ }
376
+
377
+ const basePath = getBasePath();
378
+ let currentUser = null;
379
+ let allRoles = [];
380
+ let users = [];
381
+
382
+ // Check authentication on load
383
+ window.addEventListener('DOMContentLoaded', async () => {
384
+ const token = localStorage.getItem('auth_token');
385
+ if (!token) {
386
+ window.location.href = '/auth/login';
387
+ return;
388
+ }
389
+
390
+ await loadCurrentUser();
391
+ await loadRoles();
392
+ await loadUsers();
393
+ bindEvents();
394
+ });
395
+
396
+ async function loadCurrentUser() {
397
+ try {
398
+ const token = localStorage.getItem('auth_token');
399
+ const response = await fetch(`${basePath}api/auth/me`, {
400
+ headers: {
401
+ 'Authorization': `Bearer ${token}`
402
+ }
403
+ });
404
+
405
+ if (response.ok) {
406
+ currentUser = await response.json();
407
+ document.getElementById('currentUserInfo').innerHTML = `
408
+ <span>${currentUser.email}</span>
409
+ <span>|</span>
410
+ <span>${currentUser.roles.join(', ')}</span>
411
+ <button class="btn btn-secondary" onclick="logout()" style="margin-left: 10px;">Logout</button>
412
+ `;
413
+
414
+ // Check if user has manage_users permission
415
+ if (!currentUser.roles.includes('admin')) {
416
+ showAlert('You do not have permission to manage users.', 'error');
417
+ setTimeout(() => {
418
+ window.location.href = '/';
419
+ }, 2000);
420
+ }
421
+ } else {
422
+ window.location.href = '/auth/login';
423
+ }
424
+ } catch (error) {
425
+ console.error('Error loading current user:', error);
426
+ window.location.href = '/auth/login';
427
+ }
428
+ }
429
+
430
+ async function loadRoles() {
431
+ try {
432
+ const token = localStorage.getItem('auth_token');
433
+ const response = await fetch(`${basePath}api/auth/roles`, {
434
+ headers: {
435
+ 'Authorization': `Bearer ${token}`
436
+ }
437
+ });
438
+
439
+ if (response.ok) {
440
+ allRoles = await response.json();
441
+ populateRolesCheckbox();
442
+ }
443
+ } catch (error) {
444
+ console.error('Error loading roles:', error);
445
+ }
446
+ }
447
+
448
+ function populateRolesCheckbox() {
449
+ const container = document.getElementById('rolesCheckboxGroup');
450
+ container.innerHTML = '';
451
+ allRoles.forEach(role => {
452
+ const checkbox = document.createElement('div');
453
+ checkbox.className = 'checkbox-item';
454
+ checkbox.innerHTML = `
455
+ <input type="checkbox" id="role-${role.id}" value="${role.id}">
456
+ <label for="role-${role.id}">${role.name}</label>
457
+ `;
458
+ container.appendChild(checkbox);
459
+ });
460
+ }
461
+
462
+ async function loadUsers() {
463
+ try {
464
+ const token = localStorage.getItem('auth_token');
465
+ const response = await fetch(`${basePath}api/auth/users`, {
466
+ headers: {
467
+ 'Authorization': `Bearer ${token}`
468
+ }
469
+ });
470
+
471
+ if (response.ok) {
472
+ users = await response.json();
473
+ renderUsers();
474
+ } else {
475
+ showAlert('Failed to load users.', 'error');
476
+ }
477
+ } catch (error) {
478
+ console.error('Error loading users:', error);
479
+ showAlert('Error loading users.', 'error');
480
+ }
481
+ }
482
+
483
+ function renderUsers() {
484
+ const tbody = document.getElementById('usersTableBody');
485
+
486
+ if (users.length === 0) {
487
+ tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No users found.</td></tr>';
488
+ return;
489
+ }
490
+
491
+ tbody.innerHTML = users.map(user => `
492
+ <tr>
493
+ <td>${user.email}</td>
494
+ <td>
495
+ ${user.roles.map(role => `<span class="role-badge role-${role}">${role}</span>`).join('')}
496
+ </td>
497
+ <td>
498
+ <span class="status-badge status-${user.active ? 'active' : 'inactive'}">
499
+ ${user.active ? 'Active' : 'Inactive'}
500
+ </span>
501
+ </td>
502
+ <td>${new Date(user.created_at).toLocaleDateString()}</td>
503
+ <td>
504
+ <div class="action-buttons">
505
+ <button class="btn-icon btn-edit" onclick="editUser('${user.id}')">Edit Roles</button>
506
+ </div>
507
+ </td>
508
+ </tr>
509
+ `).join('');
510
+ }
511
+
512
+ function bindEvents() {
513
+ document.getElementById('addUserBtn').addEventListener('click', () => openModal());
514
+ document.getElementById('refreshBtn').addEventListener('click', () => loadUsers());
515
+ document.getElementById('closeModalBtn').addEventListener('click', () => closeModal());
516
+ document.getElementById('cancelBtn').addEventListener('click', () => closeModal());
517
+ document.getElementById('userForm').addEventListener('submit', handleSubmit);
518
+
519
+ // Close modal on outside click
520
+ document.getElementById('userModal').addEventListener('click', (e) => {
521
+ if (e.target.id === 'userModal') {
522
+ closeModal();
523
+ }
524
+ });
525
+ }
526
+
527
+ function openModal(userId = null) {
528
+ const modal = document.getElementById('userModal');
529
+ const form = document.getElementById('userForm');
530
+ const title = document.getElementById('modalTitle');
531
+
532
+ if (userId) {
533
+ title.textContent = 'Edit User Roles';
534
+ const user = users.find(u => u.id === userId);
535
+ if (user) {
536
+ document.getElementById('userId').value = user.id;
537
+ document.getElementById('userEmail').value = user.email;
538
+ document.getElementById('userEmail').disabled = true;
539
+ document.getElementById('userPassword').required = false;
540
+ document.getElementById('userPassword').style.display = 'none';
541
+ document.getElementById('userPassword').parentElement.querySelector('small').style.display = 'none';
542
+
543
+ // Check user's current roles
544
+ allRoles.forEach(role => {
545
+ const checkbox = document.getElementById(`role-${role.id}`);
546
+ checkbox.checked = user.roles.includes(role.id);
547
+ });
548
+ }
549
+ } else {
550
+ title.textContent = 'Add User';
551
+ form.reset();
552
+ document.getElementById('userId').value = '';
553
+ document.getElementById('userEmail').disabled = false;
554
+ document.getElementById('userPassword').required = true;
555
+ document.getElementById('userPassword').style.display = 'block';
556
+ document.getElementById('userPassword').parentElement.querySelector('small').style.display = 'block';
557
+
558
+ // Uncheck all roles
559
+ allRoles.forEach(role => {
560
+ const checkbox = document.getElementById(`role-${role.id}`);
561
+ checkbox.checked = false;
562
+ });
563
+ }
564
+
565
+ modal.classList.add('show');
566
+ }
567
+
568
+ function closeModal() {
569
+ document.getElementById('userModal').classList.remove('show');
570
+ document.getElementById('userForm').reset();
571
+ }
572
+
573
+ async function handleSubmit(e) {
574
+ e.preventDefault();
575
+
576
+ const userId = document.getElementById('userId').value;
577
+ const email = document.getElementById('userEmail').value;
578
+ const password = document.getElementById('userPassword').value;
579
+ const selectedRoles = Array.from(document.querySelectorAll('#rolesCheckboxGroup input:checked'))
580
+ .map(cb => cb.value);
581
+
582
+ const token = localStorage.getItem('auth_token');
583
+
584
+ try {
585
+ if (userId) {
586
+ // Update user roles
587
+ for (const role of allRoles) {
588
+ const checkbox = document.getElementById(`role-${role.id}`);
589
+ const userHasRole = users.find(u => u.id === userId)?.roles.includes(role.id);
590
+
591
+ if (checkbox.checked && !userHasRole) {
592
+ // Add role
593
+ await fetch(`/api/auth/users/${userId}/roles`, {
594
+ method: 'POST',
595
+ headers: {
596
+ 'Content-Type': 'application/json',
597
+ 'Authorization': `Bearer ${token}`
598
+ },
599
+ body: JSON.stringify({ role: role.id })
600
+ });
601
+ } else if (!checkbox.checked && userHasRole) {
602
+ // Remove role
603
+ await fetch(`/api/auth/users/${userId}/roles/${role.id}`, {
604
+ method: 'DELETE',
605
+ headers: {
606
+ 'Authorization': `Bearer ${token}`
607
+ }
608
+ });
609
+ }
610
+ }
611
+ showAlert('User roles updated successfully!', 'success');
612
+ } else {
613
+ // Create new user
614
+ const response = await fetch(`${basePath}api/auth/users`, {
615
+ method: 'POST',
616
+ headers: {
617
+ 'Content-Type': 'application/json',
618
+ 'Authorization': `Bearer ${token}`
619
+ },
620
+ body: JSON.stringify({
621
+ email,
622
+ password,
623
+ roles: selectedRoles
624
+ })
625
+ });
626
+
627
+ if (response.ok) {
628
+ showAlert('User created successfully!', 'success');
629
+ } else {
630
+ const data = await response.json();
631
+ showAlert(data.error || 'Failed to create user.', 'error');
632
+ return;
633
+ }
634
+ }
635
+
636
+ closeModal();
637
+ await loadUsers();
638
+ } catch (error) {
639
+ console.error('Error saving user:', error);
640
+ showAlert('Error saving user.', 'error');
641
+ }
642
+ }
643
+
644
+ function editUser(userId) {
645
+ openModal(userId);
646
+ }
647
+
648
+ function showAlert(message, type) {
649
+ const container = document.getElementById('alertContainer');
650
+ const alert = document.createElement('div');
651
+ alert.className = `alert alert-${type} show`;
652
+ alert.textContent = message;
653
+ container.innerHTML = '';
654
+ container.appendChild(alert);
655
+
656
+ setTimeout(() => {
657
+ alert.classList.remove('show');
658
+ setTimeout(() => container.innerHTML = '', 300);
659
+ }, 3000);
660
+ }
661
+
662
+ function logout() {
663
+ const token = localStorage.getItem('auth_token');
664
+ if (token) {
665
+ fetch(`${basePath}api/auth/logout`, {
666
+ method: 'POST',
667
+ headers: {
668
+ 'Authorization': `Bearer ${token}`
669
+ }
670
+ });
671
+ }
672
+ localStorage.removeItem('auth_token');
673
+ localStorage.removeItem('user');
674
+ window.location.href = '/auth/login';
675
+ }
676
+ </script>
677
+ </body>
678
+ </html>
679
+