larxius commited on
Commit
221809c
·
verified ·
1 Parent(s): 2a7a083

Update frontend/src/pages/AdminPage.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/pages/AdminPage.jsx +672 -424
frontend/src/pages/AdminPage.jsx CHANGED
@@ -1,22 +1,59 @@
1
  import React, { useState, useEffect, useCallback } from 'react';
2
  import { useAuth } from '../components/AuthContext';
3
  import { CustomModal } from '../components/CustomModal';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  const AdminPageContent = () => {
6
- const { token } = useAuth();
 
 
 
7
  const [users, setUsers] = useState([]);
8
  const [organizations, setOrganizations] = useState([]);
9
  const [scanAccess, setScanAccess] = useState([]);
 
 
 
10
  const [loading, setLoading] = useState(true);
 
11
  const [error, setError] = useState('');
12
  const [message, setMessage] = useState('');
 
 
 
 
 
13
 
 
 
 
14
  const [sortUserCol, setSortUserCol] = useState('Email');
15
  const [sortUserDir, setSortUserDir] = useState('asc');
16
 
17
- const [sortOrgCol, setSortOrgCol] = useState('Created');
18
- const [sortOrgDir, setSortOrgDir] = useState('desc');
19
 
 
20
  const [promptModal, setPromptModal] = useState({ isOpen: false, title: '', desc: '', inputs: [], onConfirm: null });
21
  const [promptValues, setPromptValues] = useState({});
22
 
@@ -29,98 +66,65 @@ const AdminPageContent = () => {
29
  setPromptValues(prev => ({ ...prev, [key]: val }));
30
  };
31
 
32
- const fetchUsers = useCallback(async () => {
 
33
  try {
34
- const res = await fetch('/api/auth/users', {
35
- headers: { 'Authorization': `Bearer ${token}` },
36
- });
37
- if (res.ok) {
38
- const data = await res.json();
39
- setUsers(data.users);
40
- } else {
41
- setError('Failed to load users. Admin privileges required.');
42
  }
43
- } catch {
44
- setError('Could not connect to API.');
45
- }
46
- }, [token]);
47
 
48
- const fetchOrganizations = useCallback(async () => {
49
- try {
50
- const res = await fetch('/api/auth/organizations', {
51
- headers: { 'Authorization': `Bearer ${token}` },
52
- });
53
- if (res.ok) {
54
- const data = await res.json();
55
- setOrganizations(data.organizations || []);
 
 
 
56
  }
57
- } catch {
58
- console.error('Could not load organizations');
59
- }
60
- }, [token]);
61
 
62
- const fetchScanAccess = useCallback(async () => {
63
- try {
64
- const res = await fetch('/api/admin/scan-access', {
65
- headers: { 'Authorization': `Bearer ${token}` },
66
- });
67
- if (res.ok) {
68
- const data = await res.json();
69
- setScanAccess(data.controls || []);
70
  }
71
- } catch {
72
- console.error('Could not load scan access config');
 
 
 
 
73
  }
74
  }, [token]);
75
 
76
- const updateScanAccess = async (scanType, requiredTier, isEnabled) => {
77
- setMessage('');
78
- setError('');
79
- try {
80
- const res = await fetch(`/api/admin/scan-access/${scanType}`, {
81
- method: 'PUT',
82
- headers: {
83
- 'Content-Type': 'application/json',
84
- 'Authorization': `Bearer ${token}`,
85
- },
86
- body: JSON.stringify({ required_tier: requiredTier, is_enabled: isEnabled }),
87
- });
88
- if (res.ok) {
89
- setMessage(`${scanType} access updated successfully.`);
90
- fetchScanAccess();
91
- } else {
92
- const data = await res.json();
93
- setError(data.message || 'Failed to update access control.');
94
- }
95
- } catch {
96
- setError('Could not connect to API.');
97
- }
98
- };
99
-
100
  useEffect(() => {
101
- const fetchAll = () => {
102
- Promise.all([fetchUsers(), fetchScanAccess(), fetchOrganizations()]).finally(() => setLoading(false));
103
- };
104
- fetchAll();
105
- const interval = setInterval(fetchAll, 5000);
106
  return () => clearInterval(interval);
107
- }, [fetchUsers, fetchScanAccess, fetchOrganizations]);
108
 
 
109
  const handleRoleChange = async (userId, newRole) => {
110
  setMessage('');
111
  setError('');
112
  try {
 
113
  const res = await fetch(`/api/auth/users/${userId}/role`, {
114
  method: 'PUT',
115
  headers: {
116
  'Content-Type': 'application/json',
117
- 'Authorization': `Bearer ${token}`,
118
  },
119
  body: JSON.stringify({ role: newRole }),
120
  });
121
  if (res.ok) {
122
- setMessage(`User role updated to ${newRole}.`);
123
- fetchUsers();
124
  } else {
125
  const data = await res.json();
126
  setError(data.message || 'Failed to update role.');
@@ -134,13 +138,14 @@ const AdminPageContent = () => {
134
  setMessage('');
135
  setError('');
136
  try {
 
137
  const res = await fetch(`/api/auth/users/${userId}/unlock`, {
138
  method: 'POST',
139
- headers: { 'Authorization': `Bearer ${token}` },
140
  });
141
  if (res.ok) {
142
- setMessage('User account unlocked.');
143
- fetchUsers();
144
  } else {
145
  const data = await res.json();
146
  setError(data.message || 'Failed to unlock user.');
@@ -150,97 +155,13 @@ const AdminPageContent = () => {
150
  }
151
  };
152
 
153
- const handleAssignScans = (org) => {
154
- setPromptValues({ scan_type: 'Deep', count: '1' });
155
- setPromptModal({
156
- isOpen: true,
157
- title: 'Assign Custom Scans',
158
- desc: `Grant specific scan limits for ${org.name}`,
159
- inputs: [
160
- {
161
- key: 'scan_type',
162
- label: 'Scan Type',
163
- type: 'select',
164
- options: ['Quick', 'Advanced', 'Deep']
165
- },
166
- { key: 'count', label: 'Number of Scans', placeholder: 'e.g., 5' }
167
- ],
168
- onConfirm: async (values) => {
169
- const addedCount = parseInt(values.count);
170
- if (isNaN(addedCount) || addedCount <= 0) {
171
- setError('Please enter a valid scan count.');
172
- closePrompt();
173
- return;
174
- }
175
- try {
176
- const res = await fetch(`/api/auth/organizations/${org.id}/quotas`, {
177
- method: 'POST',
178
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
179
- body: JSON.stringify({ scan_type: values.scan_type, count: addedCount })
180
- });
181
- if (res.ok) {
182
- setMessage(`${addedCount} ${values.scan_type} scan(s) assigned to ${org.name}.`);
183
- // Optimistic instant UI state update
184
- setOrganizations(prevOrgs => prevOrgs.map(o => {
185
- if (o.id === org.id) {
186
- const existingQuotas = o.quotas || [];
187
- let found = false;
188
- const updatedQuotas = existingQuotas.map(q => {
189
- if (q.scan_type?.toLowerCase() === values.scan_type?.toLowerCase()) {
190
- found = true;
191
- return {
192
- ...q,
193
- allocated_count: q.allocated_count === -1 ? -1 : (q.allocated_count || 0) + addedCount
194
- };
195
- }
196
- return q;
197
- });
198
- if (!found) {
199
- updatedQuotas.push({ scan_type: values.scan_type, allocated_count: addedCount, used_count: 0 });
200
- }
201
- return { ...o, quotas: updatedQuotas };
202
- }
203
- return o;
204
- }));
205
- fetchOrganizations();
206
- } else {
207
- const data = await res.json();
208
- setError(data.message || 'Failed to assign scans.');
209
- }
210
- } catch {
211
- setError('Could not connect to API for assigning scans.');
212
- }
213
- closePrompt();
214
- }
215
- });
216
- };
217
-
218
- const handleImpersonate = async (orgId, orgName) => {
219
- try {
220
- const res = await fetch(`/api/auth/impersonate/${orgId}`, {
221
- method: 'POST',
222
- headers: { 'Authorization': `Bearer ${token}` }
223
- });
224
- if (res.ok) {
225
- const data = await res.json();
226
- localStorage.setItem('original_admin_token', token);
227
- localStorage.setItem('wss_token', data.access_token);
228
- window.location.href = '/dashboard';
229
- } else {
230
- const data = await res.json();
231
- setError(data.message || 'Failed to impersonate organization.');
232
- }
233
- } catch {
234
- setError('Could not connect to API for impersonation.');
235
- }
236
- };
237
-
238
  const handleProvisionTenant = () => {
239
  setPromptValues({ tier: 'none', name: '', admin_email: '' });
240
  setPromptModal({
241
  isOpen: true,
242
  title: 'Add New Organization',
243
- desc: 'Create a new tenant organization.',
244
  inputs: [
245
  { key: 'name', label: 'Organization Name', placeholder: 'Enter organization name...' },
246
  {
@@ -252,7 +173,7 @@ const AdminPageContent = () => {
252
  { label: 'Quick', value: 'quick' },
253
  { label: 'Advanced', value: 'advanced' },
254
  { label: 'Deep', value: 'deep' },
255
- { label: 'Enterprise(Custom)', value: 'Enterprise(Custom)' }
256
  ]
257
  },
258
  { key: 'admin_email', label: 'Admin Email (Optional)', placeholder: 'admin@company.com' }
@@ -264,14 +185,15 @@ const AdminPageContent = () => {
264
  return;
265
  }
266
  try {
 
267
  const res = await fetch('/api/auth/organizations', {
268
  method: 'POST',
269
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
270
  body: JSON.stringify({ name: values.name, tier: values.tier, admin_email: values.admin_email })
271
  });
272
  if (res.ok) {
273
  setMessage('Organization created successfully.');
274
- fetchOrganizations();
275
  } else {
276
  const data = await res.json();
277
  setError(data.message || 'Failed to create organization.');
@@ -293,14 +215,11 @@ const AdminPageContent = () => {
293
  ? rawTier.toLowerCase()
294
  : 'none';
295
 
296
- setPromptValues({
297
- name: org.name,
298
- tier: initialTier
299
- });
300
  setPromptModal({
301
  isOpen: true,
302
  title: 'Edit Organization',
303
- desc: `Modify settings for ${org.name}`,
304
  inputs: [
305
  { key: 'name', label: 'Organization Name', placeholder: 'Enter organization name...' },
306
  {
@@ -312,20 +231,21 @@ const AdminPageContent = () => {
312
  { label: 'Quick', value: 'quick' },
313
  { label: 'Advanced', value: 'advanced' },
314
  { label: 'Deep', value: 'deep' },
315
- { label: 'Enterprise(Custom)', value: 'Enterprise(Custom)' }
316
  ]
317
  }
318
  ],
319
  onConfirm: async (values) => {
320
  try {
 
321
  const res = await fetch(`/api/auth/organizations/${org.id}`, {
322
  method: 'PUT',
323
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
324
  body: JSON.stringify({ name: values.name, tier: values.tier })
325
  });
326
  if (res.ok) {
327
  setMessage('Organization updated successfully.');
328
- fetchOrganizations();
329
  } else {
330
  const data = await res.json();
331
  setError(data.message || 'Failed to update organization.');
@@ -338,42 +258,118 @@ const AdminPageContent = () => {
338
  });
339
  };
340
 
341
- if (loading) {
342
- return (
343
- <div className="flex items-center justify-center py-2xl font-label-md text-label-md text-on-surface-variant">
344
- <span className="material-symbols-outlined animate-spin mr-sm">sync</span>
345
- Loading users...
346
- </div>
347
- );
348
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
- const handleUserSort = (column) => {
351
- if (column === 'Actions' || column === 'Role') return;
352
- if (sortUserCol === column) {
353
- setSortUserDir(sortUserDir === 'asc' ? 'desc' : 'asc');
354
- } else {
355
- setSortUserCol(column);
356
- setSortUserDir('asc');
 
 
 
 
 
 
 
 
 
 
 
357
  }
358
  };
359
 
360
- const handleOrgSort = (column) => {
361
- if (column === 'Actions') return;
362
- if (sortOrgCol === column) {
363
- setSortOrgDir(sortOrgDir === 'asc' ? 'desc' : 'asc');
364
- } else {
365
- setSortOrgCol(column);
366
- setSortOrgDir('asc');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  }
368
  };
369
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  const getSortedUsers = () => {
371
- return [...users].sort((a, b) => {
 
372
  let aVal, bVal;
373
  switch (sortUserCol) {
374
  case 'Email': aVal = a.email || ''; bVal = b.email || ''; break;
 
 
375
  case 'Status': aVal = a.locked_until ? 1 : 0; bVal = b.locked_until ? 1 : 0; break;
376
- default: return 0;
377
  }
378
  if (aVal < bVal) return sortUserDir === 'asc' ? -1 : 1;
379
  if (aVal > bVal) return sortUserDir === 'asc' ? 1 : -1;
@@ -381,14 +377,32 @@ const AdminPageContent = () => {
381
  });
382
  };
383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  const getSortedOrgs = () => {
385
- return [...organizations].sort((a, b) => {
 
386
  let aVal, bVal;
387
  switch (sortOrgCol) {
388
  case 'Tenant Name': aVal = a.name || ''; bVal = b.name || ''; break;
389
- case 'Tier': aVal = a.subscription_tier || ''; bVal = b.subscription_tier || ''; break;
390
- case 'Created': aVal = new Date(a.created_at || 0).getTime(); bVal = new Date(b.created_at || 0).getTime(); break;
391
- default: return 0;
392
  }
393
  if (aVal < bVal) return sortOrgDir === 'asc' ? -1 : 1;
394
  if (aVal > bVal) return sortOrgDir === 'asc' ? 1 : -1;
@@ -396,253 +410,483 @@ const AdminPageContent = () => {
396
  });
397
  };
398
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  return (
400
- <div className="flex flex-col gap-gutter">
401
- <div className="border-b border-outline-variant bg-surface-container-lowest p-lg rounded-xl shadow-sm">
402
- <h1 className="font-display-lg text-display-lg text-on-surface mb-sm font-bold tracking-tight">Admin Panel</h1>
403
- <p className="font-body-lg text-body-lg text-on-surface-variant">Manage users, roles, and account access.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  </div>
405
 
 
406
  {message && (
407
- <div className="flex gap-sm bg-green-500/10 border border-green-500/30 rounded-lg p-md text-green-600 font-body-sm text-body-sm items-center">
408
- <span className="material-symbols-outlined shrink-0 text-green-500">check_circle</span>
409
- <div>{message}</div>
410
  </div>
411
  )}
412
 
413
  {error && (
414
- <div className="flex gap-sm bg-error-container/20 border border-error/30 rounded-lg p-md text-error font-body-sm text-body-sm items-center">
415
- <span className="material-symbols-outlined shrink-0">error</span>
416
- <div>{error}</div>
417
  </div>
418
  )}
419
 
420
- <div className="bg-surface-container-lowest border border-outline-variant rounded-lg shadow-sm overflow-hidden">
421
- <table className="w-full">
422
- <thead>
423
- <tr className="border-b border-outline-variant bg-surface-container-high select-none">
424
- {['Email', 'Role', 'Status', 'Actions'].map((h, i) => (
425
- <th
426
- key={h}
427
- onClick={() => handleUserSort(h)}
428
- className={`text-left px-lg py-md font-label-sm text-label-sm text-on-surface-variant uppercase tracking-wider ${i === 3 ? 'text-right' : ''} ${(h !== 'Actions' && h !== 'Role') ? 'cursor-pointer hover:bg-surface-container-highest transition-colors group' : ''}`}
429
- >
430
- <div className={`flex items-center gap-xs ${i === 3 ? 'justify-end' : ''}`}>
431
- {h}
432
- {(h !== 'Actions' && h !== 'Role') && (
433
- <span className={`material-symbols-outlined text-[16px] opacity-0 group-hover:opacity-50 transition-opacity ${sortUserCol === h ? 'opacity-100 group-hover:opacity-100 text-primary' : ''}`}>
434
- {sortUserCol === h && sortUserDir === 'desc' ? 'arrow_downward' : 'arrow_upward'}
435
- </span>
436
- )}
437
- </div>
438
- </th>
439
- ))}
440
- </tr>
441
- </thead>
442
- <tbody>
443
- {getSortedUsers().map((u) => (
444
- <tr key={u.id} className="border-b border-outline-variant/60 last:border-0 hover:bg-surface-container-high/50 transition-colors">
445
- <td className="px-lg py-md font-body-md text-on-surface">{u.email}</td>
446
- <td className="px-lg py-md">
447
- <select
448
- value={u.role || 'read_only'}
449
- onChange={(e) => handleRoleChange(u.id, e.target.value)}
450
- className="bg-surface-container border border-outline-variant rounded px-sm py-xs font-body-sm text-on-surface cursor-pointer"
451
- >
452
- <option value="super_admin">Super Admin</option>
453
- <option value="admin">Admin</option>
454
- <option value="support_engineer">Support Engineer</option>
455
- <option value="org_admin">Organization</option>
456
- <option value="soc_analyst">SOC Analyst</option>
457
- <option value="executive">Executive</option>
458
- <option value="read_only">Read Only</option>
459
- </select>
460
- </td>
461
- <td className="px-lg py-md">
462
- {u.locked_until ? (
463
- <span className="inline-flex items-center gap-xs bg-error-container/20 text-error px-sm py-xs rounded font-label-sm text-label-sm">
464
- <span className="material-symbols-outlined text-[16px]">lock</span>
465
- Locked
466
- </span>
467
- ) : (
468
- <span className="inline-flex items-center gap-xs bg-green-500/10 text-green-600 px-sm py-xs rounded font-label-sm text-label-sm">
469
- <span className="material-symbols-outlined text-[16px]">check_circle</span>
470
- Active
471
- </span>
472
- )}
473
- </td>
474
- <td className="px-lg py-md text-right">
475
- {u.locked_until && (
476
- <button
477
- onClick={() => handleUnlock(u.id)}
478
- className="bg-primary text-on-primary px-md py-xs rounded font-label-sm text-label-sm hover:opacity-90 transition-opacity border-0 cursor-pointer"
479
- >
480
- Unlock
481
- </button>
482
- )}
483
- </td>
484
- </tr>
485
- ))}
486
- </tbody>
487
- </table>
488
  </div>
489
- <div className="mt-xl border-b border-outline-variant bg-surface-container-lowest p-lg rounded-xl shadow-sm">
490
- <div className="flex flex-col sm:flex-row sm:items-center justify-between mb-lg gap-sm">
491
- <div>
492
- <h2 className="font-headline-md text-headline-md text-on-surface mb-xs font-bold tracking-tight">Organizations</h2>
493
- <p className="font-body-md text-body-md text-on-surface-variant">
494
- Manage tenant organizations, subscription tiers, and scan quotas.
495
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
  </div>
497
- <button
498
- onClick={handleProvisionTenant}
499
- className="bg-primary text-white px-md py-sm rounded-lg font-label-md text-label-md hover:brightness-110 transition-all font-bold border-0 cursor-pointer flex items-center gap-xs shadow-md shadow-primary/20 self-start sm:self-auto whitespace-nowrap"
500
- >
501
- <span className="material-symbols-outlined text-[18px]">add_business</span>
502
- Add Organization
503
- </button>
504
- </div>
505
 
506
- <div className="bg-surface-container-lowest border border-outline-variant rounded-lg shadow-sm overflow-hidden">
507
- <table className="w-full">
508
- <thead>
509
- <tr className="border-b border-outline-variant bg-surface-container-high select-none">
510
- {['Tenant Name', 'Tier', 'Quotas', 'Created', 'Actions'].map((h, i) => (
511
- <th
512
- key={h}
513
- onClick={() => handleOrgSort(h)}
514
- className={`text-left px-lg py-md font-label-sm text-label-sm text-on-surface-variant uppercase tracking-wider ${i === 4 ? 'text-right' : ''} ${(h !== 'Actions' && h !== 'Quotas') ? 'cursor-pointer hover:bg-surface-container-highest transition-colors group' : ''}`}
515
- >
516
- <div className={`flex items-center gap-xs ${i === 4 ? 'justify-end' : ''}`}>
517
- {h}
518
- {(h !== 'Actions' && h !== 'Quotas') && (
519
- <span className={`material-symbols-outlined text-[16px] opacity-0 group-hover:opacity-50 transition-opacity ${sortOrgCol === h ? 'opacity-100 group-hover:opacity-100 text-primary' : ''}`}>
520
- {sortOrgCol === h && sortOrgDir === 'desc' ? 'arrow_downward' : 'arrow_upward'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  )}
523
- </div>
524
- </th>
525
  ))}
526
- </tr>
527
- </thead>
528
- <tbody>
529
- {getSortedOrgs().map((org) => (
530
- <tr key={org.id} className="border-b border-outline-variant/60 last:border-0 hover:bg-surface-container-high/50 transition-colors">
531
- <td className="px-lg py-md font-label-md font-bold text-on-surface">{org.name}</td>
532
- <td className="px-lg py-md font-body-sm capitalize">{org.subscription_tier || 'Free'}</td>
533
- <td className="px-lg py-md font-body-sm">
534
- <div className="flex flex-wrap gap-1.5 items-center">
535
- {org.quotas?.map((q, idx) => {
536
- const remaining = q.allocated_count === -1 ? '∞' : Math.max(0, q.allocated_count - (q.used_count || 0));
537
- const style = q.scan_type === 'Deep' ? 'bg-orange-500/10 text-orange-600 border-orange-500/30' :
538
- q.scan_type === 'Advanced' ? 'bg-purple-500/10 text-purple-600 border-purple-500/30' :
539
- 'bg-blue-500/10 text-blue-600 border-blue-500/30';
540
- return (
541
- <div key={idx} className={`text-[10.5px] font-bold px-2 py-0.5 rounded border flex items-center gap-1 shadow-sm ${style}`}>
542
- <span className="uppercase opacity-90 tracking-wider">{q.scan_type}:</span>
543
- <span className="text-[12px]">{remaining}</span>
544
- </div>
545
- );
546
- })}
547
- </div>
548
- </td>
549
- <td className="px-lg py-md font-body-sm text-on-surface-variant">
550
- {org.created_at ? new Date(org.created_at).toLocaleDateString() : 'N/A'}
551
- </td>
552
- <td className="px-lg py-md text-right">
553
- <button
554
- onClick={() => handleEditTenant(org)}
555
- className="text-on-surface-variant hover:text-primary transition-colors bg-transparent border-0 cursor-pointer p-1 inline-flex items-center gap-xs mr-2"
556
- title="Edit Organization"
557
- >
558
- <span className="material-symbols-outlined text-[18px]">edit</span>
559
- <span className="font-label-sm">Edit</span>
560
- </button>
561
- <button
562
- onClick={() => handleAssignScans(org)}
563
- className="text-on-surface-variant hover:text-primary transition-colors bg-transparent border-0 cursor-pointer p-1 inline-flex items-center gap-xs mr-2"
564
- title="Assign Custom Scans"
565
- >
566
- <span className="material-symbols-outlined text-[18px]">add_box</span>
567
- <span className="font-label-sm">Assign Scans</span>
568
- </button>
569
- <button
570
- onClick={() => handleImpersonate(org.id, org.name)}
571
- className="text-on-surface-variant hover:text-primary transition-colors bg-transparent border-0 cursor-pointer p-1 inline-flex items-center gap-xs"
572
- title="View Dashboard Data"
 
 
 
 
 
573
  >
574
- <span className="material-symbols-outlined text-[18px]">vpn_key</span>
575
- <span className="font-label-sm">Impersonate</span>
576
- </button>
577
- </td>
578
  </tr>
579
- ))}
580
- {organizations.length === 0 && (
581
- <tr>
582
- <td colSpan="5" className="px-lg py-xl text-center text-on-surface-variant font-body-md">
583
- No organizations found.
584
- </td>
585
- </tr>
586
- )}
587
- </tbody>
588
- </table>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  </div>
590
- </div>
591
 
592
- <div className="mt-xl border-b border-outline-variant bg-surface-container-lowest p-lg rounded-xl shadow-sm">
593
- <h2 className="font-headline-md text-headline-md text-on-surface mb-sm font-bold tracking-tight">Scanner Modes & Access</h2>
594
- <p className="font-body-md text-body-md text-on-surface-variant mb-lg">
595
- Configure which subscription plans grant access to specific scan modes. You can also completely enable or disable scan modes globally.
596
- </p>
597
-
598
- <div className="flex flex-col gap-md">
599
- {(scanAccess || []).map((mode) => (
600
- <div key={mode.scan_type} className="bg-surface-container-low border border-outline-variant rounded-lg p-md flex items-center justify-between">
601
- <div className="flex flex-col">
602
- <span className="font-label-md text-label-md text-on-surface font-bold">{mode.scan_type} Scan</span>
603
- <span className="font-body-sm text-body-sm text-on-surface-variant">Global Access: {mode.is_enabled ? 'Enabled' : 'Disabled'}</span>
604
- </div>
605
-
606
- <div className="flex items-center gap-lg">
607
- <div className="flex flex-col gap-xs">
608
- <label className="font-label-sm text-label-sm text-on-surface-variant uppercase tracking-wider font-semibold">
609
- Minimum Plan Required
610
- </label>
611
- <select
612
- value={mode.required_tier}
613
- onChange={(e) => updateScanAccess(mode.scan_type, e.target.value, mode.is_enabled)}
614
- className="bg-surface-container border border-outline-variant rounded px-sm py-xs font-body-sm text-on-surface cursor-pointer focus:outline-none focus:border-primary"
615
- >
616
- <option value="quick">Quick</option>
617
- <option value="advanced">Advanced</option>
618
- <option value="deep">Deep</option>
619
- <option value="Enterprise(Custom)">Enterprise(Custom)</option>
620
- </select>
621
  </div>
622
 
623
- <div className="flex flex-col gap-xs">
624
- <label className="font-label-sm text-label-sm text-on-surface-variant uppercase tracking-wider font-semibold">
625
- Enable Scan Mode
626
- </label>
627
- <label className="flex items-center cursor-pointer">
628
- <div className="relative">
629
- <input
630
- type="checkbox"
631
- className="sr-only"
632
- checked={mode.is_enabled}
633
- onChange={(e) => updateScanAccess(mode.scan_type, mode.required_tier, e.target.checked)}
634
- />
635
- <div className={`block w-10 h-6 rounded-full transition-colors ${mode.is_enabled ? 'bg-primary' : 'bg-surface-container-highest'}`}></div>
636
- <div className={`dot absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${mode.is_enabled ? 'transform translate-x-4' : ''}`}></div>
637
- </div>
638
- </label>
 
 
 
 
 
 
 
 
 
 
639
  </div>
640
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
641
  </div>
642
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
643
  </div>
644
- </div>
645
 
 
646
  <CustomModal
647
  isOpen={promptModal.isOpen}
648
  onClose={closePrompt}
@@ -651,7 +895,7 @@ const AdminPageContent = () => {
651
  footer={
652
  <>
653
  <button onClick={closePrompt} className="px-4 py-2 text-on-surface-variant hover:bg-surface-container rounded-lg font-bold border-0 bg-transparent cursor-pointer">Cancel</button>
654
- <button onClick={() => promptModal.onConfirm(promptValues)} className="px-4 py-2 bg-primary text-on-primary rounded-lg font-bold border-0 cursor-pointer">Confirm</button>
655
  </>
656
  }
657
  >
@@ -663,9 +907,15 @@ const AdminPageContent = () => {
663
  <select
664
  value={promptValues[input.key] || ''}
665
  onChange={(e) => handlePromptChange(input.key, e.target.value)}
666
- className="bg-surface-container border border-outline-variant rounded-lg px-3 py-2 focus:border-primary outline-none text-on-surface"
667
  >
668
- {input.options.map(opt => <option key={opt} value={opt}>{opt}</option>)}
 
 
 
 
 
 
669
  </select>
670
  ) : (
671
  <input
@@ -673,7 +923,7 @@ const AdminPageContent = () => {
673
  value={promptValues[input.key] || ''}
674
  onChange={(e) => handlePromptChange(input.key, e.target.value)}
675
  placeholder={input.placeholder}
676
- className="bg-surface-container border border-outline-variant rounded-lg px-3 py-2 focus:border-primary outline-none text-on-surface"
677
  />
678
  )}
679
  </div>
@@ -684,8 +934,6 @@ const AdminPageContent = () => {
684
  );
685
  };
686
 
687
- import { ErrorBoundary } from '../components/ErrorBoundary';
688
-
689
  export const AdminPage = () => (
690
  <ErrorBoundary>
691
  <AdminPageContent />
 
1
  import React, { useState, useEffect, useCallback } from 'react';
2
  import { useAuth } from '../components/AuthContext';
3
  import { CustomModal } from '../components/CustomModal';
4
+ import { useNavigate } from 'react-router-dom';
5
+ import {
6
+ Shield,
7
+ Users,
8
+ Building2,
9
+ Activity,
10
+ ShieldAlert,
11
+ Plus,
12
+ Edit3,
13
+ Key,
14
+ Search,
15
+ RefreshCw,
16
+ Lock,
17
+ Unlock,
18
+ CheckCircle2,
19
+ AlertCircle,
20
+ Sliders,
21
+ FileText,
22
+ BarChart3
23
+ } from 'lucide-react';
24
+ import { ErrorBoundary } from '../components/ErrorBoundary';
25
 
26
  const AdminPageContent = () => {
27
+ const { token, user } = useAuth();
28
+ const navigate = useNavigate();
29
+
30
+ const [activeTab, setActiveTab] = useState('members'); // 'members', 'orgs', 'scan_access', 'audit'
31
  const [users, setUsers] = useState([]);
32
  const [organizations, setOrganizations] = useState([]);
33
  const [scanAccess, setScanAccess] = useState([]);
34
+ const [auditLogs, setAuditLogs] = useState([]);
35
+ const [metrics, setMetrics] = useState(null);
36
+
37
  const [loading, setLoading] = useState(true);
38
+ const [syncing, setSyncing] = useState(false);
39
  const [error, setError] = useState('');
40
  const [message, setMessage] = useState('');
41
+
42
+ // Search & Filter States
43
+ const [userSearch, setUserSearch] = useState('');
44
+ const [userRoleFilter, setUserRoleFilter] = useState('all');
45
+ const [userOrgFilter, setUserOrgFilter] = useState('all');
46
 
47
+ const [orgSearch, setOrgSearch] = useState('');
48
+
49
+ // Sorting States
50
  const [sortUserCol, setSortUserCol] = useState('Email');
51
  const [sortUserDir, setSortUserDir] = useState('asc');
52
 
53
+ const [sortOrgCol, setSortOrgCol] = useState('Tenant Name');
54
+ const [sortOrgDir, setSortOrgDir] = useState('asc');
55
 
56
+ // Modal States
57
  const [promptModal, setPromptModal] = useState({ isOpen: false, title: '', desc: '', inputs: [], onConfirm: null });
58
  const [promptValues, setPromptValues] = useState({});
59
 
 
66
  setPromptValues(prev => ({ ...prev, [key]: val }));
67
  };
68
 
69
+ const fetchAllData = useCallback(async () => {
70
+ setSyncing(true);
71
  try {
72
+ const activeToken = localStorage.getItem('wss_token') || token;
73
+ if (!activeToken) {
74
+ setLoading(false);
75
+ setSyncing(false);
76
+ return;
 
 
 
77
  }
 
 
 
 
78
 
79
+ const [statsRes, accessRes] = await Promise.all([
80
+ fetch('/api/global-stats', { headers: { 'Authorization': `Bearer ${activeToken}` } }),
81
+ fetch('/api/admin/scan-access', { headers: { 'Authorization': `Bearer ${activeToken}` } })
82
+ ]);
83
+
84
+ if (statsRes.ok) {
85
+ const statsData = await statsRes.json();
86
+ setMetrics(statsData.metrics || null);
87
+ setOrganizations(statsData.tenants || []);
88
+ setUsers(statsData.users || []);
89
+ setAuditLogs(statsData.audit_logs || []);
90
  }
 
 
 
 
91
 
92
+ if (accessRes.ok) {
93
+ const accessData = await accessRes.json();
94
+ setScanAccess(accessData.controls || []);
 
 
 
 
 
95
  }
96
+ } catch (err) {
97
+ console.error('Failed to load admin data:', err);
98
+ setError('Could not connect to backend server.');
99
+ } finally {
100
+ setLoading(false);
101
+ setSyncing(false);
102
  }
103
  }, [token]);
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  useEffect(() => {
106
+ fetchAllData();
107
+ const interval = setInterval(fetchAllData, 5000);
 
 
 
108
  return () => clearInterval(interval);
109
+ }, [fetchAllData]);
110
 
111
+ // User Actions
112
  const handleRoleChange = async (userId, newRole) => {
113
  setMessage('');
114
  setError('');
115
  try {
116
+ const activeToken = localStorage.getItem('wss_token') || token;
117
  const res = await fetch(`/api/auth/users/${userId}/role`, {
118
  method: 'PUT',
119
  headers: {
120
  'Content-Type': 'application/json',
121
+ 'Authorization': `Bearer ${activeToken}`,
122
  },
123
  body: JSON.stringify({ role: newRole }),
124
  });
125
  if (res.ok) {
126
+ setMessage(`User role successfully updated to ${newRole}.`);
127
+ fetchAllData();
128
  } else {
129
  const data = await res.json();
130
  setError(data.message || 'Failed to update role.');
 
138
  setMessage('');
139
  setError('');
140
  try {
141
+ const activeToken = localStorage.getItem('wss_token') || token;
142
  const res = await fetch(`/api/auth/users/${userId}/unlock`, {
143
  method: 'POST',
144
+ headers: { 'Authorization': `Bearer ${activeToken}` },
145
  });
146
  if (res.ok) {
147
+ setMessage('User account unlocked successfully.');
148
+ fetchAllData();
149
  } else {
150
  const data = await res.json();
151
  setError(data.message || 'Failed to unlock user.');
 
155
  }
156
  };
157
 
158
+ // Organization Actions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  const handleProvisionTenant = () => {
160
  setPromptValues({ tier: 'none', name: '', admin_email: '' });
161
  setPromptModal({
162
  isOpen: true,
163
  title: 'Add New Organization',
164
+ desc: 'Create a new tenant organization in LarShield.',
165
  inputs: [
166
  { key: 'name', label: 'Organization Name', placeholder: 'Enter organization name...' },
167
  {
 
173
  { label: 'Quick', value: 'quick' },
174
  { label: 'Advanced', value: 'advanced' },
175
  { label: 'Deep', value: 'deep' },
176
+ { label: 'Enterprise (Custom)', value: 'Enterprise(Custom)' }
177
  ]
178
  },
179
  { key: 'admin_email', label: 'Admin Email (Optional)', placeholder: 'admin@company.com' }
 
185
  return;
186
  }
187
  try {
188
+ const activeToken = localStorage.getItem('wss_token') || token;
189
  const res = await fetch('/api/auth/organizations', {
190
  method: 'POST',
191
+ headers: { 'Authorization': `Bearer ${activeToken}`, 'Content-Type': 'application/json' },
192
  body: JSON.stringify({ name: values.name, tier: values.tier, admin_email: values.admin_email })
193
  });
194
  if (res.ok) {
195
  setMessage('Organization created successfully.');
196
+ fetchAllData();
197
  } else {
198
  const data = await res.json();
199
  setError(data.message || 'Failed to create organization.');
 
215
  ? rawTier.toLowerCase()
216
  : 'none';
217
 
218
+ setPromptValues({ name: org.name, tier: initialTier });
 
 
 
219
  setPromptModal({
220
  isOpen: true,
221
  title: 'Edit Organization',
222
+ desc: `Modify subscription tier and name for ${org.name}`,
223
  inputs: [
224
  { key: 'name', label: 'Organization Name', placeholder: 'Enter organization name...' },
225
  {
 
231
  { label: 'Quick', value: 'quick' },
232
  { label: 'Advanced', value: 'advanced' },
233
  { label: 'Deep', value: 'deep' },
234
+ { label: 'Enterprise (Custom)', value: 'Enterprise(Custom)' }
235
  ]
236
  }
237
  ],
238
  onConfirm: async (values) => {
239
  try {
240
+ const activeToken = localStorage.getItem('wss_token') || token;
241
  const res = await fetch(`/api/auth/organizations/${org.id}`, {
242
  method: 'PUT',
243
+ headers: { 'Authorization': `Bearer ${activeToken}`, 'Content-Type': 'application/json' },
244
  body: JSON.stringify({ name: values.name, tier: values.tier })
245
  });
246
  if (res.ok) {
247
  setMessage('Organization updated successfully.');
248
+ fetchAllData();
249
  } else {
250
  const data = await res.json();
251
  setError(data.message || 'Failed to update organization.');
 
258
  });
259
  };
260
 
261
+ const handleAssignScans = (org) => {
262
+ setPromptValues({ scan_type: 'Deep', count: '1' });
263
+ setPromptModal({
264
+ isOpen: true,
265
+ title: 'Assign Custom Scans',
266
+ desc: `Grant specific scan limits for ${org.name}`,
267
+ inputs: [
268
+ {
269
+ key: 'scan_type',
270
+ label: 'Scan Type',
271
+ type: 'select',
272
+ options: ['Quick', 'Advanced', 'Deep']
273
+ },
274
+ { key: 'count', label: 'Number of Scans', placeholder: 'e.g., 5' }
275
+ ],
276
+ onConfirm: async (values) => {
277
+ const addedCount = parseInt(values.count);
278
+ if (isNaN(addedCount) || addedCount <= 0) {
279
+ setError('Please enter a valid scan count.');
280
+ closePrompt();
281
+ return;
282
+ }
283
+ try {
284
+ const activeToken = localStorage.getItem('wss_token') || token;
285
+ const res = await fetch(`/api/auth/organizations/${org.id}/quotas`, {
286
+ method: 'POST',
287
+ headers: { 'Authorization': `Bearer ${activeToken}`, 'Content-Type': 'application/json' },
288
+ body: JSON.stringify({ scan_type: values.scan_type, count: addedCount })
289
+ });
290
+ if (res.ok) {
291
+ setMessage(`${addedCount} ${values.scan_type} scan(s) assigned to ${org.name}.`);
292
+ fetchAllData();
293
+ } else {
294
+ const data = await res.json();
295
+ setError(data.message || 'Failed to assign scans.');
296
+ }
297
+ } catch {
298
+ setError('Could not connect to API for assigning scans.');
299
+ }
300
+ closePrompt();
301
+ }
302
+ });
303
+ };
304
 
305
+ const handleImpersonate = async (orgId, orgName) => {
306
+ try {
307
+ const activeToken = localStorage.getItem('wss_token') || token;
308
+ const res = await fetch(`/api/auth/impersonate/${orgId}`, {
309
+ method: 'POST',
310
+ headers: { 'Authorization': `Bearer ${activeToken}` }
311
+ });
312
+ if (res.ok) {
313
+ const data = await res.json();
314
+ localStorage.setItem('original_admin_token', activeToken);
315
+ localStorage.setItem('wss_token', data.access_token);
316
+ window.location.href = '/dashboard';
317
+ } else {
318
+ const data = await res.json();
319
+ setError(data.message || 'Failed to impersonate organization.');
320
+ }
321
+ } catch {
322
+ setError('Could not connect to API for impersonation.');
323
  }
324
  };
325
 
326
+ const updateScanAccess = async (scanType, requiredTier, isEnabled) => {
327
+ setMessage('');
328
+ setError('');
329
+ try {
330
+ const activeToken = localStorage.getItem('wss_token') || token;
331
+ const res = await fetch(`/api/admin/scan-access/${scanType}`, {
332
+ method: 'PUT',
333
+ headers: {
334
+ 'Content-Type': 'application/json',
335
+ 'Authorization': `Bearer ${activeToken}`,
336
+ },
337
+ body: JSON.stringify({ required_tier: requiredTier, is_enabled: isEnabled }),
338
+ });
339
+ if (res.ok) {
340
+ setMessage(`${scanType} scan access rules updated.`);
341
+ fetchAllData();
342
+ } else {
343
+ const data = await res.json();
344
+ setError(data.message || 'Failed to update scan access control.');
345
+ }
346
+ } catch {
347
+ setError('Could not connect to API.');
348
  }
349
  };
350
 
351
+ // User Filter & Sort Logic
352
+ const getFilteredUsers = () => {
353
+ return (users || []).filter(u => {
354
+ const emailMatch = !userSearch || u.email?.toLowerCase().includes(userSearch.toLowerCase()) || u.org_name?.toLowerCase().includes(userSearch.toLowerCase());
355
+ const roleMatch = userRoleFilter === 'all' || u.role === userRoleFilter;
356
+ const orgMatch = userOrgFilter === 'all' || (
357
+ userOrgFilter === 'no_org' ? (!u.org_id || u.org_name?.startsWith('No Org')) : String(u.org_id) === String(userOrgFilter)
358
+ );
359
+ return emailMatch && roleMatch && orgMatch;
360
+ });
361
+ };
362
+
363
  const getSortedUsers = () => {
364
+ const list = getFilteredUsers();
365
+ return list.sort((a, b) => {
366
  let aVal, bVal;
367
  switch (sortUserCol) {
368
  case 'Email': aVal = a.email || ''; bVal = b.email || ''; break;
369
+ case 'Role': aVal = a.role || ''; bVal = b.role || ''; break;
370
+ case 'Organization': aVal = a.org_name || ''; bVal = b.org_name || ''; break;
371
  case 'Status': aVal = a.locked_until ? 1 : 0; bVal = b.locked_until ? 1 : 0; break;
372
+ default: aVal = a.email || ''; bVal = b.email || '';
373
  }
374
  if (aVal < bVal) return sortUserDir === 'asc' ? -1 : 1;
375
  if (aVal > bVal) return sortUserDir === 'asc' ? 1 : -1;
 
377
  });
378
  };
379
 
380
+ const handleUserSort = (col) => {
381
+ if (col === 'Actions') return;
382
+ if (sortUserCol === col) {
383
+ setSortUserDir(sortUserDir === 'asc' ? 'desc' : 'asc');
384
+ } else {
385
+ setSortUserCol(col);
386
+ setSortUserDir('asc');
387
+ }
388
+ };
389
+
390
+ // Org Filter & Sort Logic
391
+ const getFilteredOrgs = () => {
392
+ return (organizations || []).filter(org => {
393
+ return !orgSearch || org.name?.toLowerCase().includes(orgSearch.toLowerCase()) || org.tier?.toLowerCase().includes(orgSearch.toLowerCase());
394
+ });
395
+ };
396
+
397
  const getSortedOrgs = () => {
398
+ const list = getFilteredOrgs();
399
+ return list.sort((a, b) => {
400
  let aVal, bVal;
401
  switch (sortOrgCol) {
402
  case 'Tenant Name': aVal = a.name || ''; bVal = b.name || ''; break;
403
+ case 'Tier': aVal = a.tier || a.subscription_tier || ''; bVal = b.tier || b.subscription_tier || ''; break;
404
+ case 'Created': aVal = new Date(a.created || a.created_at || 0).getTime(); bVal = new Date(b.created || b.created_at || 0).getTime(); break;
405
+ default: aVal = a.name || ''; bVal = b.name || '';
406
  }
407
  if (aVal < bVal) return sortOrgDir === 'asc' ? -1 : 1;
408
  if (aVal > bVal) return sortOrgDir === 'asc' ? 1 : -1;
 
410
  });
411
  };
412
 
413
+ const handleOrgSort = (col) => {
414
+ if (col === 'Actions' || col === 'Quotas') return;
415
+ if (sortOrgCol === col) {
416
+ setSortOrgDir(sortOrgDir === 'asc' ? 'desc' : 'asc');
417
+ } else {
418
+ setSortOrgCol(col);
419
+ setSortOrgDir('asc');
420
+ }
421
+ };
422
+
423
+ if (loading) {
424
+ return (
425
+ <div className="flex h-[80vh] items-center justify-center">
426
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
427
+ </div>
428
+ );
429
+ }
430
+
431
+ const uniqueOrgsList = Array.from(new Set((users || []).map(u => u.org_name).filter(Boolean)));
432
+
433
  return (
434
+ <div className="w-full text-on-surface animate-fade-in pb-xl">
435
+ {/* Header Banner */}
436
+ <div className="flex flex-col md:flex-row md:items-center justify-between mb-xl gap-sm border-b border-outline-variant/60 pb-md">
437
+ <div>
438
+ <h1 className="font-extrabold text-on-surface tracking-tight text-[24px] m-0 flex items-center gap-1.5">
439
+ Admin <span className="text-primary">Management Console</span>
440
+ </h1>
441
+ <p className="font-body-md text-on-surface-variant text-[13.5px] mt-1 m-0">
442
+ Global client oversight, organization provisioning, user role management, and system logs.
443
+ </p>
444
+ </div>
445
+ <div className="flex gap-sm flex-wrap items-center">
446
+ <button
447
+ onClick={fetchAllData}
448
+ className="flex items-center px-3.5 py-1.5 bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[12.5px] cursor-pointer shadow-2xs"
449
+ >
450
+ <RefreshCw className={`w-3.5 h-3.5 mr-1.5 text-primary ${syncing ? 'animate-spin' : ''}`} /> Sync Metrics
451
+ </button>
452
+ <button
453
+ onClick={() => navigate('/organization')}
454
+ className="flex items-center px-3.5 py-1.5 bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[12.5px] cursor-pointer shadow-2xs"
455
+ >
456
+ <BarChart3 className="w-3.5 h-3.5 mr-1.5 text-primary" /> Org Dashboard
457
+ </button>
458
+ <button
459
+ onClick={() => navigate('/super-admin/logs')}
460
+ className="flex items-center px-3.5 py-1.5 bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[12.5px] cursor-pointer shadow-2xs"
461
+ >
462
+ <ShieldAlert className="w-3.5 h-3.5 mr-1.5 text-primary" /> Logs & Threats
463
+ </button>
464
+ <button
465
+ onClick={handleProvisionTenant}
466
+ className="flex items-center px-4 py-1.5 bg-primary text-white rounded-lg hover:brightness-110 transition-all font-bold text-[13px] border-0 cursor-pointer shadow-sm"
467
+ >
468
+ <Plus className="w-4 h-4 mr-1" /> Add Organization
469
+ </button>
470
+ </div>
471
  </div>
472
 
473
+ {/* Notifications */}
474
  {message && (
475
+ <div className="flex gap-2 bg-green-500/10 border border-green-500/30 rounded-xl p-3 mb-md text-green-400 font-bold text-[13px] items-center animate-fade-in">
476
+ <CheckCircle2 className="w-4 h-4 shrink-0 text-green-500" />
477
+ <span>{message}</span>
478
  </div>
479
  )}
480
 
481
  {error && (
482
+ <div className="flex gap-2 bg-error/10 border border-error/30 rounded-xl p-3 mb-md text-error font-bold text-[13px] items-center animate-fade-in">
483
+ <AlertCircle className="w-4 h-4 shrink-0" />
484
+ <span>{error}</span>
485
  </div>
486
  )}
487
 
488
+ {/* Metric Cards Grid */}
489
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-md mb-xl">
490
+ {[
491
+ { title: 'TOTAL TENANTS', value: (metrics?.total_tenants || organizations.length).toString(), icon: Building2, color: 'text-blue-500', bg: 'bg-blue-500/10 border-blue-500/20' },
492
+ { title: 'GLOBAL USERS', value: (metrics?.global_users || users.length).toString(), icon: Users, color: 'text-purple-500', bg: 'bg-purple-500/10 border-purple-500/20' },
493
+ { title: 'ACTIVE LICENSES', value: (metrics?.active_licenses || 0).toString(), icon: Shield, color: 'text-green-500', bg: 'bg-green-500/10 border-green-500/20' },
494
+ { title: 'ACTIVE SCANNERS', value: (metrics?.active_scanners || 0).toString(), icon: Activity, color: 'text-orange-500', bg: 'bg-orange-500/10 border-orange-500/20' }
495
+ ].map((m, i) => (
496
+ <div key={i} className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-2xs hover:shadow-md transition-all group">
497
+ <div className="flex justify-between items-center">
498
+ <div>
499
+ <p className="text-on-surface-variant font-bold text-[12px] uppercase tracking-wider mb-1.5">{m.title}</p>
500
+ <h3 className="text-[30px] font-extrabold tracking-tight text-on-surface leading-none">{m.value}</h3>
501
+ </div>
502
+ <div className={`${m.bg} p-3 rounded-xl border group-hover:scale-110 transition-transform flex items-center justify-center`}>
503
+ <m.icon className={`${m.color} w-6 h-6`} />
504
+ </div>
505
+ </div>
506
+ </div>
507
+ ))}
508
+ </div>
509
+
510
+ {/* Tab Controls Bar */}
511
+ <div className="flex items-center gap-2 mb-lg border-b border-outline-variant/60 pb-sm">
512
+ <button
513
+ onClick={() => setActiveTab('members')}
514
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg font-bold text-[13px] transition-all cursor-pointer border-0 ${
515
+ activeTab === 'members'
516
+ ? 'bg-primary text-white shadow-md shadow-primary/20'
517
+ : 'bg-surface-container text-on-surface-variant hover:bg-surface-container-high'
518
+ }`}
519
+ >
520
+ <Users className="w-4 h-4" /> Global Members ({users.length})
521
+ </button>
522
+
523
+ <button
524
+ onClick={() => setActiveTab('orgs')}
525
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg font-bold text-[13px] transition-all cursor-pointer border-0 ${
526
+ activeTab === 'orgs'
527
+ ? 'bg-primary text-white shadow-md shadow-primary/20'
528
+ : 'bg-surface-container text-on-surface-variant hover:bg-surface-container-high'
529
+ }`}
530
+ >
531
+ <Building2 className="w-4 h-4" /> Organizations & Quotas ({organizations.length})
532
+ </button>
533
+
534
+ <button
535
+ onClick={() => setActiveTab('scan_access')}
536
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg font-bold text-[13px] transition-all cursor-pointer border-0 ${
537
+ activeTab === 'scan_access'
538
+ ? 'bg-primary text-white shadow-md shadow-primary/20'
539
+ : 'bg-surface-container text-on-surface-variant hover:bg-surface-container-high'
540
+ }`}
541
+ >
542
+ <Sliders className="w-4 h-4" /> Scanner Modes & Access
543
+ </button>
544
+
545
+ <button
546
+ onClick={() => setActiveTab('audit')}
547
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg font-bold text-[13px] transition-all cursor-pointer border-0 ${
548
+ activeTab === 'audit'
549
+ ? 'bg-primary text-white shadow-md shadow-primary/20'
550
+ : 'bg-surface-container text-on-surface-variant hover:bg-surface-container-high'
551
+ }`}
552
+ >
553
+ <FileText className="w-4 h-4" /> Audit Trail
554
+ </button>
 
555
  </div>
556
+
557
+ {/* Tab 1: Global Members */}
558
+ {activeTab === 'members' && (
559
+ <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl shadow-sm overflow-hidden p-lg">
560
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-md mb-lg">
561
+ <div>
562
+ <h2 className="font-extrabold text-on-surface text-[18px] m-0">Global User Accounts</h2>
563
+ <p className="text-on-surface-variant text-[13px] mt-0.5 m-0">Manage roles, permissions, and account status across all organizations.</p>
564
+ </div>
565
+
566
+ <div className="flex gap-sm flex-wrap w-full md:w-auto">
567
+ <div className="relative flex-1 md:w-64">
568
+ <Search className="w-4 h-4 absolute left-3 top-2.5 text-on-surface-variant" />
569
+ <input
570
+ type="text"
571
+ placeholder="Search email or org..."
572
+ value={userSearch}
573
+ onChange={e => setUserSearch(e.target.value)}
574
+ className="w-full bg-surface-container border border-outline-variant rounded-lg pl-9 pr-3 py-1.5 text-[13px] outline-none focus:border-primary text-on-surface font-medium"
575
+ />
576
+ </div>
577
+
578
+ <select
579
+ value={userRoleFilter}
580
+ onChange={e => setUserRoleFilter(e.target.value)}
581
+ className="bg-surface-container border border-outline-variant rounded-lg px-3 py-1.5 text-[13px] font-bold outline-none text-on-surface"
582
+ >
583
+ <option value="all">All Roles</option>
584
+ <option value="super_admin">Super Admin</option>
585
+ <option value="admin">Admin</option>
586
+ <option value="support_engineer">Support Engineer</option>
587
+ <option value="org_admin">Organization Admin</option>
588
+ <option value="soc_analyst">SOC Analyst</option>
589
+ <option value="executive_user">Executive</option>
590
+ <option value="read_only">Read Only</option>
591
+ </select>
592
+
593
+ <select
594
+ value={userOrgFilter}
595
+ onChange={e => setUserOrgFilter(e.target.value)}
596
+ className="bg-surface-container border border-outline-variant rounded-lg px-3 py-1.5 text-[13px] font-bold outline-none text-on-surface"
597
+ >
598
+ <option value="all">All Organizations</option>
599
+ <option value="no_org">No Org (Global Role)</option>
600
+ {organizations.map(org => <option key={org.id} value={org.id}>{org.name}</option>)}
601
+ </select>
602
+ </div>
603
  </div>
 
 
 
 
 
 
 
 
604
 
605
+ <div className="overflow-x-auto">
606
+ <table className="w-full text-left border-collapse">
607
+ <thead>
608
+ <tr className="border-b border-outline-variant bg-surface-container-high/60 select-none">
609
+ {['Email', 'Role', 'Organization', 'Status', 'Actions'].map((col) => (
610
+ <th
611
+ key={col}
612
+ onClick={() => handleUserSort(col)}
613
+ className={`px-4 py-3 text-[12px] font-bold uppercase tracking-wider text-on-surface-variant ${col === 'Actions' ? 'text-right' : 'cursor-pointer hover:text-primary'}`}
614
+ >
615
+ {col} {sortUserCol === col ? (sortUserDir === 'asc' ? '' : '') : ''}
616
+ </th>
617
+ ))}
618
+ </tr>
619
+ </thead>
620
+ <tbody>
621
+ {getSortedUsers().map((u) => (
622
+ <tr key={u.id} className="border-b border-outline-variant/40 hover:bg-surface-container-high/30 transition-colors">
623
+ <td className="px-4 py-3 font-bold text-[13.5px] text-on-surface">{u.email}</td>
624
+ <td className="px-4 py-3">
625
+ <select
626
+ value={u.role || 'read_only'}
627
+ onChange={(e) => handleRoleChange(u.id, e.target.value)}
628
+ className="bg-surface-container border border-outline-variant rounded-lg px-2.5 py-1 text-[12.5px] font-bold text-on-surface outline-none cursor-pointer"
629
+ >
630
+ <option value="super_admin">Super Admin</option>
631
+ <option value="admin">Admin</option>
632
+ <option value="support_engineer">Support Engineer</option>
633
+ <option value="org_admin">Org Admin</option>
634
+ <option value="soc_analyst">SOC Analyst</option>
635
+ <option value="executive_user">Executive</option>
636
+ <option value="read_only">Read Only</option>
637
+ </select>
638
+ </td>
639
+ <td className="px-4 py-3 text-[13px] font-medium text-on-surface-variant">
640
+ {u.org_name || 'No Org'}
641
+ </td>
642
+ <td className="px-4 py-3">
643
+ {u.locked_until ? (
644
+ <span className="inline-flex items-center gap-1 bg-error/10 text-error px-2.5 py-0.5 rounded-full text-[11.5px] font-bold">
645
+ <Lock className="w-3 h-3" /> Locked
646
  </span>
647
+ ) : (
648
+ <span className="inline-flex items-center gap-1 bg-green-500/10 text-green-500 px-2.5 py-0.5 rounded-full text-[11.5px] font-bold">
649
+ <CheckCircle2 className="w-3 h-3" /> Active
650
+ </span>
651
+ )}
652
+ </td>
653
+ <td className="px-4 py-3 text-right">
654
+ {u.locked_until && (
655
+ <button
656
+ onClick={() => handleUnlock(u.id)}
657
+ className="px-3 py-1 bg-primary text-white rounded-lg text-[12px] font-bold hover:brightness-110 transition-all border-0 cursor-pointer shadow-xs inline-flex items-center gap-1"
658
+ >
659
+ <Unlock className="w-3 h-3" /> Unlock
660
+ </button>
661
  )}
662
+ </td>
663
+ </tr>
664
  ))}
665
+ {getSortedUsers().length === 0 && (
666
+ <tr>
667
+ <td colSpan="5" className="text-center py-xl text-on-surface-variant font-medium text-[13px]">
668
+ No users match the current search filters.
669
+ </td>
670
+ </tr>
671
+ )}
672
+ </tbody>
673
+ </table>
674
+ </div>
675
+ </div>
676
+ )}
677
+
678
+ {/* Tab 2: Organizations */}
679
+ {activeTab === 'orgs' && (
680
+ <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl shadow-sm overflow-hidden p-lg">
681
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-md mb-lg">
682
+ <div>
683
+ <h2 className="font-extrabold text-on-surface text-[18px] m-0">Registered Organizations</h2>
684
+ <p className="text-on-surface-variant text-[13px] mt-0.5 m-0">Provision tenant accounts, update subscription tiers, and allocate scan quotas.</p>
685
+ </div>
686
+
687
+ <div className="flex gap-sm w-full md:w-auto">
688
+ <div className="relative flex-1 md:w-64">
689
+ <Search className="w-4 h-4 absolute left-3 top-2.5 text-on-surface-variant" />
690
+ <input
691
+ type="text"
692
+ placeholder="Search organization name..."
693
+ value={orgSearch}
694
+ onChange={e => setOrgSearch(e.target.value)}
695
+ className="w-full bg-surface-container border border-outline-variant rounded-lg pl-9 pr-3 py-1.5 text-[13px] outline-none focus:border-primary text-on-surface font-medium"
696
+ />
697
+ </div>
698
+
699
+ <button
700
+ onClick={handleProvisionTenant}
701
+ className="px-3.5 py-1.5 bg-primary text-white rounded-lg font-bold text-[13px] hover:brightness-110 transition-all border-0 cursor-pointer shadow-sm flex items-center gap-1 whitespace-nowrap"
702
+ >
703
+ <Plus className="w-4 h-4" /> Add Tenant
704
+ </button>
705
+ </div>
706
+ </div>
707
+
708
+ <div className="overflow-x-auto">
709
+ <table className="w-full text-left border-collapse">
710
+ <thead>
711
+ <tr className="border-b border-outline-variant bg-surface-container-high/60 select-none">
712
+ {['Tenant Name', 'Tier', 'Quotas', 'Created', 'Actions'].map((col) => (
713
+ <th
714
+ key={col}
715
+ onClick={() => handleOrgSort(col)}
716
+ className={`px-4 py-3 text-[12px] font-bold uppercase tracking-wider text-on-surface-variant ${col === 'Actions' ? 'text-right' : 'cursor-pointer hover:text-primary'}`}
717
  >
718
+ {col} {sortOrgCol === col ? (sortOrgDir === 'asc' ? '↑' : '↓') : ''}
719
+ </th>
720
+ ))}
 
721
  </tr>
722
+ </thead>
723
+ <tbody>
724
+ {getSortedOrgs().map((org) => (
725
+ <tr key={org.id} className="border-b border-outline-variant/40 hover:bg-surface-container-high/30 transition-colors">
726
+ <td className="px-4 py-3 font-extrabold text-[14px] text-on-surface">{org.name}</td>
727
+ <td className="px-4 py-3 text-[13px] font-bold capitalize text-primary">
728
+ {org.tier || org.subscription_tier || 'Free'}
729
+ </td>
730
+ <td className="px-4 py-3">
731
+ <div className="flex flex-wrap gap-1.5 items-center">
732
+ {org.quotas?.map((q, idx) => {
733
+ const remaining = q.allocated_count === -1 ? '∞' : Math.max(0, q.allocated_count - (q.used_count || 0));
734
+ const style = q.scan_type === 'Deep' ? 'bg-orange-500/10 text-orange-400 border-orange-500/30' :
735
+ q.scan_type === 'Advanced' ? 'bg-purple-500/10 text-purple-400 border-purple-500/30' :
736
+ 'bg-blue-500/10 text-blue-400 border-blue-500/30';
737
+ return (
738
+ <div key={idx} className={`text-[10.5px] font-extrabold px-2 py-0.5 rounded border flex items-center gap-1 shadow-2xs ${style}`}>
739
+ <span className="uppercase tracking-wider">{q.scan_type}:</span>
740
+ <span className="text-[12px]">{remaining}</span>
741
+ </div>
742
+ );
743
+ })}
744
+ </div>
745
+ </td>
746
+ <td className="px-4 py-3 text-[12.5px] font-medium text-on-surface-variant">
747
+ {org.created ? org.created : (org.created_at ? new Date(org.created_at).toLocaleDateString() : 'N/A')}
748
+ </td>
749
+ <td className="px-4 py-3 text-right">
750
+ <div className="flex items-center justify-end gap-1">
751
+ <button
752
+ onClick={() => handleEditTenant(org)}
753
+ className="px-2.5 py-1 bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[12px] cursor-pointer flex items-center gap-1"
754
+ >
755
+ <Edit3 className="w-3.5 h-3.5 text-primary" /> Edit
756
+ </button>
757
+ <button
758
+ onClick={() => handleAssignScans(org)}
759
+ className="px-2.5 py-1 bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[12px] cursor-pointer flex items-center gap-1"
760
+ >
761
+ <Plus className="w-3.5 h-3.5 text-primary" /> Quotas
762
+ </button>
763
+ <button
764
+ onClick={() => handleImpersonate(org.id, org.name)}
765
+ className="px-2.5 py-1 bg-primary/10 text-primary border border-primary/20 rounded-lg hover:bg-primary/20 transition-colors font-bold text-[12px] cursor-pointer flex items-center gap-1"
766
+ >
767
+ <Key className="w-3.5 h-3.5" /> Impersonate
768
+ </button>
769
+ </div>
770
+ </td>
771
+ </tr>
772
+ ))}
773
+ {getSortedOrgs().length === 0 && (
774
+ <tr>
775
+ <td colSpan="5" className="text-center py-xl text-on-surface-variant font-medium text-[13px]">
776
+ No organizations found.
777
+ </td>
778
+ </tr>
779
+ )}
780
+ </tbody>
781
+ </table>
782
+ </div>
783
  </div>
784
+ )}
785
 
786
+ {/* Tab 3: Scanner Modes & Access */}
787
+ {activeTab === 'scan_access' && (
788
+ <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl shadow-sm p-lg">
789
+ <h2 className="font-extrabold text-on-surface text-[18px] mb-1">Scanner Engine Access Controls</h2>
790
+ <p className="text-on-surface-variant text-[13.5px] mb-lg">
791
+ Configure global subscription tier requirements and enable/disable specific scan engines platform-wide.
792
+ </p>
793
+
794
+ <div className="grid grid-cols-1 gap-md">
795
+ {(scanAccess || []).map((mode) => (
796
+ <div key={mode.scan_type} className="bg-surface-container-low border border-outline-variant/60 rounded-xl p-md flex flex-col md:flex-row md:items-center justify-between gap-md">
797
+ <div>
798
+ <h4 className="font-extrabold text-on-surface text-[15px] m-0">{mode.scan_type} Scan Engine</h4>
799
+ <p className="text-on-surface-variant text-[12.5px] mt-0.5 m-0">
800
+ Status: <span className={mode.is_enabled ? 'text-green-400 font-bold' : 'text-error font-bold'}>{mode.is_enabled ? 'Globally Enabled' : 'Globally Disabled'}</span>
801
+ </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
802
  </div>
803
 
804
+ <div className="flex items-center gap-lg">
805
+ <div className="flex flex-col gap-1">
806
+ <label className="text-[11px] font-bold uppercase tracking-wider text-on-surface-variant">Required Plan Tier</label>
807
+ <select
808
+ value={mode.required_tier}
809
+ onChange={(e) => updateScanAccess(mode.scan_type, e.target.value, mode.is_enabled)}
810
+ className="bg-surface-container border border-outline-variant rounded-lg px-3 py-1.5 text-[13px] font-bold text-on-surface outline-none cursor-pointer"
811
+ >
812
+ <option value="quick">Quick</option>
813
+ <option value="advanced">Advanced</option>
814
+ <option value="deep">Deep</option>
815
+ <option value="Enterprise(Custom)">Enterprise(Custom)</option>
816
+ </select>
817
+ </div>
818
+
819
+ <div className="flex flex-col gap-1">
820
+ <label className="text-[11px] font-bold uppercase tracking-wider text-on-surface-variant">Engine Switch</label>
821
+ <button
822
+ onClick={() => updateScanAccess(mode.scan_type, mode.required_tier, !mode.is_enabled)}
823
+ className={`px-4 py-1.5 rounded-lg text-[12.5px] font-bold cursor-pointer transition-all border-0 ${
824
+ mode.is_enabled ? 'bg-green-500/20 text-green-400 hover:bg-green-500/30' : 'bg-error/20 text-error hover:bg-error/30'
825
+ }`}
826
+ >
827
+ {mode.is_enabled ? 'Enabled' : 'Disabled'}
828
+ </button>
829
+ </div>
830
  </div>
831
  </div>
832
+ ))}
833
+ </div>
834
+ </div>
835
+ )}
836
+
837
+ {/* Tab 4: Audit Trail Preview */}
838
+ {activeTab === 'audit' && (
839
+ <div className="bg-surface-container-lowest border border-outline-variant rounded-2xl shadow-sm p-lg">
840
+ <div className="flex justify-between items-center mb-md">
841
+ <div>
842
+ <h2 className="font-extrabold text-on-surface text-[18px] m-0">Recent Audit Trail</h2>
843
+ <p className="text-on-surface-variant text-[13px] mt-0.5 m-0">Security actions and administrative audit logs.</p>
844
  </div>
845
+ <button
846
+ onClick={() => navigate('/super-admin/logs')}
847
+ className="px-3.5 py-1.5 bg-primary text-white rounded-lg font-bold text-[12.5px] hover:brightness-110 transition-all border-0 cursor-pointer flex items-center gap-1 shadow-xs"
848
+ >
849
+ <FileText className="w-3.5 h-3.5" /> Full Audit Logs
850
+ </button>
851
+ </div>
852
+
853
+ <div className="overflow-x-auto">
854
+ <table className="w-full text-left border-collapse">
855
+ <thead>
856
+ <tr className="border-b border-outline-variant bg-surface-container-high/60">
857
+ <th className="px-4 py-3 text-[12px] font-bold uppercase tracking-wider text-on-surface-variant">TIMESTAMP</th>
858
+ <th className="px-4 py-3 text-[12px] font-bold uppercase tracking-wider text-on-surface-variant">PERFORMED BY</th>
859
+ <th className="px-4 py-3 text-[12px] font-bold uppercase tracking-wider text-on-surface-variant">ACTION & DETAILS</th>
860
+ </tr>
861
+ </thead>
862
+ <tbody>
863
+ {auditLogs.slice(0, 10).map((log) => (
864
+ <tr key={log.id} className="border-b border-outline-variant/40 hover:bg-surface-container-high/30 transition-colors">
865
+ <td className="px-4 py-3 text-[12.5px] font-medium text-on-surface-variant whitespace-nowrap">
866
+ {log.timestamp || 'N/A'}
867
+ </td>
868
+ <td className="px-4 py-3 font-bold text-[13px] text-primary">
869
+ {log.user_email || log.admin_id || 'System'}
870
+ </td>
871
+ <td className="px-4 py-3 text-[13px] font-medium text-on-surface">
872
+ {log.action}
873
+ </td>
874
+ </tr>
875
+ ))}
876
+ {auditLogs.length === 0 && (
877
+ <tr>
878
+ <td colSpan="3" className="text-center py-xl text-on-surface-variant font-medium text-[13px]">
879
+ No audit logs available.
880
+ </td>
881
+ </tr>
882
+ )}
883
+ </tbody>
884
+ </table>
885
+ </div>
886
  </div>
887
+ )}
888
 
889
+ {/* Modal for adding/editing tenant & assigning quotas */}
890
  <CustomModal
891
  isOpen={promptModal.isOpen}
892
  onClose={closePrompt}
 
895
  footer={
896
  <>
897
  <button onClick={closePrompt} className="px-4 py-2 text-on-surface-variant hover:bg-surface-container rounded-lg font-bold border-0 bg-transparent cursor-pointer">Cancel</button>
898
+ <button onClick={() => promptModal.onConfirm(promptValues)} className="px-4 py-2 bg-primary text-white rounded-lg font-bold border-0 cursor-pointer shadow-md shadow-primary/20">Confirm</button>
899
  </>
900
  }
901
  >
 
907
  <select
908
  value={promptValues[input.key] || ''}
909
  onChange={(e) => handlePromptChange(input.key, e.target.value)}
910
+ className="bg-surface-container border border-outline-variant rounded-lg px-3 py-2 focus:border-primary outline-none text-on-surface font-medium"
911
  >
912
+ {input.options.map(opt => (
913
+ typeof opt === 'object' ? (
914
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
915
+ ) : (
916
+ <option key={opt} value={opt}>{opt}</option>
917
+ )
918
+ ))}
919
  </select>
920
  ) : (
921
  <input
 
923
  value={promptValues[input.key] || ''}
924
  onChange={(e) => handlePromptChange(input.key, e.target.value)}
925
  placeholder={input.placeholder}
926
+ className="bg-surface-container border border-outline-variant rounded-lg px-3 py-2 focus:border-primary outline-none text-on-surface font-medium"
927
  />
928
  )}
929
  </div>
 
934
  );
935
  };
936
 
 
 
937
  export const AdminPage = () => (
938
  <ErrorBoundary>
939
  <AdminPageContent />