larxius commited on
Commit
ca5e972
·
verified ·
1 Parent(s): 00a396d

Update frontend/src/pages/OrganizationPage.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/pages/OrganizationPage.jsx +378 -373
frontend/src/pages/OrganizationPage.jsx CHANGED
@@ -1,373 +1,378 @@
1
- import React, { useState, useEffect, useCallback } from 'react';
2
- import { useAuth } from '../components/AuthContext';
3
- import { Shield, Activity, Users, Globe, Lock, ShieldAlert, ArrowLeft, BarChart3, PieChart as PieChartIcon } from 'lucide-react';
4
- import { useNavigate } from 'react-router-dom';
5
- import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell, Legend, LabelList, ComposedChart } from 'recharts';
6
-
7
- const SEVERITY_COLORS = ['#EF4444', '#F97316', '#EAB308', '#3B82F6']; // Critical, High, Medium, Low
8
- const SCAN_TYPE_COLORS = ['#3B82F6', '#8B5CF6', '#10B981', '#F59E0B'];
9
-
10
- export const OrganizationPage = () => {
11
- const { user, token } = useAuth();
12
- const navigate = useNavigate();
13
- const [loading, setLoading] = useState(true);
14
-
15
- const [summaryData, setSummaryData] = useState(null);
16
- const [scanHistory, setScanHistory] = useState([]);
17
- const [filterOrg, setFilterOrg] = useState('All');
18
- const [filterWebsite, setFilterWebsite] = useState('All');
19
-
20
- const getToken = useCallback(() => localStorage.getItem('wss_token') || localStorage.getItem('wss_token') || token, [token]);
21
-
22
- const fetchData = useCallback(async () => {
23
- try {
24
- const activeToken = getToken();
25
- const [summaryRes, historyRes] = await Promise.all([
26
- fetch('/api/vulnerabilities/summary?global=true', { headers: { 'Authorization': `Bearer ${activeToken}` } }),
27
- fetch('/api/scans/history?global=true&limit=100', { headers: { 'Authorization': `Bearer ${activeToken}` } })
28
- ]);
29
-
30
- if (!summaryRes.ok || !historyRes.ok) return;
31
-
32
- const summaryJson = await summaryRes.json();
33
- const historyJson = await historyRes.json();
34
-
35
- setSummaryData(summaryJson.summary);
36
- setScanHistory(historyJson.scans || []);
37
- } catch (err) {
38
- console.error("Failed to fetch organization data:", err);
39
- } finally {
40
- setLoading(false);
41
- }
42
- }, [getToken]);
43
-
44
- useEffect(() => {
45
- fetchData();
46
- const interval = setInterval(fetchData, 5000); // Polling real-time data every 5s
47
- return () => clearInterval(interval);
48
- }, [fetchData]);
49
-
50
- if (loading) {
51
- return (
52
- <div className="flex h-[80vh] items-center justify-center">
53
- <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
54
- </div>
55
- );
56
- }
57
-
58
- // Extract Unique Orgs & Websites for filters
59
- const uniqueOrgs = [...new Set(scanHistory.map(s => s.org_name).filter(Boolean))];
60
- const uniqueWebsites = [...new Set(scanHistory.map(s => s.target_url).filter(Boolean))];
61
-
62
- // Apply Filters
63
- const filteredScans = scanHistory
64
- .filter(scan => filterOrg === 'All' || scan.org_name === filterOrg)
65
- .filter(scan => filterWebsite === 'All' || scan.target_url === filterWebsite);
66
-
67
- // Derive metrics dynamically from filteredScans
68
- const completedScans = filteredScans.filter(s => s.status === 'completed' && s.security_score !== null);
69
- const score = completedScans.length > 0
70
- ? Math.round(completedScans.reduce((sum, s) => sum + s.security_score, 0) / completedScans.length)
71
- : 100;
72
-
73
- const totalVulnerabilities = filteredScans.reduce((sum, s) => {
74
- const vc = s.vulnerabilities_count;
75
- return sum + (vc ? (vc.critical + vc.high + vc.medium + vc.low) : (s.total_vulnerabilities || 0));
76
- }, 0);
77
-
78
- // Active Assets (unique target URLs)
79
- const activeAssets = new Set(filteredScans.map(s => s.target_url).filter(Boolean)).size;
80
-
81
- // Vulnerability Distribution Pie Chart Data
82
- const vulnCounts = filteredScans.reduce((acc, s) => {
83
- if (s.vulnerabilities_count) {
84
- acc.critical += s.vulnerabilities_count.critical || 0;
85
- acc.high += s.vulnerabilities_count.high || 0;
86
- acc.medium += s.vulnerabilities_count.medium || 0;
87
- acc.low += s.vulnerabilities_count.low || 0;
88
- }
89
- return acc;
90
- }, { critical: 0, high: 0, medium: 0, low: 0 });
91
-
92
- const vulnerabilityTypes = [
93
- { name: 'Critical', value: vulnCounts.critical },
94
- { name: 'High', value: vulnCounts.high },
95
- { name: 'Medium', value: vulnCounts.medium },
96
- { name: 'Low', value: vulnCounts.low },
97
- ].filter(v => v.value > 0);
98
-
99
- if (vulnerabilityTypes.length === 0) {
100
- vulnerabilityTypes.push({ name: 'Clean / No Risks', value: 1 });
101
- }
102
-
103
- // Risk Score Trend (Last 10 Scans)
104
- const riskTrendData = [...filteredScans]
105
- .filter(s => s.started_at)
106
- .sort((a, b) => new Date(a.started_at) - new Date(b.started_at))
107
- .slice(-10)
108
- .map((scan, index) => {
109
- const totalVulns = scan.vulnerabilities_count
110
- ? (scan.vulnerabilities_count.critical + scan.vulnerabilities_count.high + scan.vulnerabilities_count.medium + scan.vulnerabilities_count.low)
111
- : (scan.total_vulnerabilities || 0);
112
- return {
113
- name: scan.target_url ? scan.target_url.replace('https://', '').replace('http://', '').replace(/\/$/, '') : `Scan ${index + 1}`,
114
- securityScore: Math.round(scan.security_score ?? 100),
115
- vulnerabilities: totalVulns
116
- };
117
- });
118
-
119
- // Group Scans by Month for Bar Chart
120
- const monthsData = {};
121
- filteredScans.forEach(scan => {
122
- const d = new Date(scan.started_at || scan.created_at);
123
- if (isNaN(d.getTime())) return;
124
- const month = d.toLocaleString('en-US', { month: 'short', year: 'numeric' });
125
- if (!monthsData[month]) monthsData[month] = { month, scans: 0, issues: 0 };
126
- monthsData[month].scans += 1;
127
- const totalVulns = scan.vulnerabilities_count
128
- ? (scan.vulnerabilities_count.critical + scan.vulnerabilities_count.high + scan.vulnerabilities_count.medium + scan.vulnerabilities_count.low)
129
- : (scan.total_vulnerabilities || 0);
130
- monthsData[month].issues += totalVulns;
131
- });
132
- const scanHistoryChartData = Object.values(monthsData);
133
-
134
- // Top Vulnerability Categories Chart Data
135
- const categoriesData = Object.entries(summaryData?.by_category || {})
136
- .map(([cat, count]) => ({ category: cat || 'General Security', count }))
137
- .sort((a, b) => b.count - a.count)
138
- .slice(0, 6);
139
-
140
- // Extract Unique Orgs & Websites for filters is moved up.
141
-
142
- // Scan Types Breakdown Chart Data (Filtered)
143
- const scanTypeCounts = {};
144
- filteredScans.forEach(scan => {
145
- const type = scan.scan_type ? (scan.scan_type.charAt(0).toUpperCase() + scan.scan_type.slice(1)) + ' Scan' : 'Advanced Scan';
146
- scanTypeCounts[type] = (scanTypeCounts[type] || 0) + 1;
147
- });
148
- const scanTypeChartData = Object.entries(scanTypeCounts).map(([name, value]) => ({ name, value }));
149
-
150
- return (
151
- <div className="w-full text-on-surface animate-fade-in pb-xl">
152
- <div className="flex flex-col md:flex-row md:items-center justify-between mb-xl gap-sm">
153
- <div>
154
- <h1 className="text-[28px] font-extrabold font-display tracking-tight brand-gradient flex items-center gap-2">
155
- <Globe className="w-8 h-8 text-primary" />
156
- LarShield Global Management
157
- </h1>
158
- <p className="text-on-surface-variant text-[14px] mt-1">Centralized oversight for all client organizations, scans, and security nodes.</p>
159
- </div>
160
- <div className="flex gap-sm flex-wrap">
161
- <button onClick={fetchData} className="flex items-center px-md py-sm bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[13.5px] cursor-pointer">
162
- <Activity className={`w-4 h-4 mr-2 text-primary ${loading ? 'animate-spin' : ''}`} /> Sync Metrics
163
- </button>
164
- <button onClick={() => navigate('/super-admin')} className="flex items-center px-md py-sm bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[13.5px] cursor-pointer">
165
- <Lock className="w-4 h-4 mr-2 text-primary" /> Manage Pricing
166
- </button>
167
- <button onClick={() => navigate('/organization')} className="flex items-center px-md py-sm bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[13.5px] cursor-pointer">
168
- <Activity className="w-4 h-4 mr-2 text-primary" /> Org Dashboard
169
- </button>
170
- <button onClick={() => navigate('/super-admin/logs')} className="flex items-center px-md py-sm bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[13.5px] cursor-pointer">
171
- <ShieldAlert className="w-4 h-4 mr-2 text-primary" /> Logs & Threats
172
- </button>
173
- <button onClick={() => navigate(-1)} className="flex items-center px-md py-sm bg-primary text-white rounded-lg hover:brightness-110 transition-all font-bold text-[13.5px] border-0 cursor-pointer shadow-md shadow-primary/20">
174
- <ArrowLeft className="w-4 h-4 mr-2" /> Back
175
- </button>
176
- </div>
177
- </div>
178
-
179
-
180
-
181
- {/* Top Metrics Grid */}
182
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-md mb-xl">
183
- {[
184
- { title: 'Security Score', value: `${score}/100`, icon: Shield, color: score > 80 ? 'text-green-500' : score > 50 ? 'text-orange-500' : 'text-error', bg: score > 80 ? 'bg-green-500/10' : score > 50 ? 'bg-orange-500/10' : 'bg-error/10', border: score > 80 ? 'border-green-500/20' : score > 50 ? 'border-orange-500/20' : 'border-error/20' },
185
- { title: 'Active Assets', value: activeAssets.toString(), icon: Globe, color: 'text-blue-500', bg: 'bg-blue-500/10', border: 'border-blue-500/20' },
186
- { title: 'Total Scans', value: filteredScans.length.toString(), icon: Activity, color: 'text-purple-500', bg: 'bg-purple-500/10', border: 'border-purple-500/20' },
187
- { title: 'Open Risks', value: totalVulnerabilities.toString(), icon: ShieldAlert, color: totalVulnerabilities > 0 ? 'text-orange-500' : 'text-green-500', bg: totalVulnerabilities > 0 ? 'bg-orange-500/10' : 'bg-green-500/10', border: totalVulnerabilities > 0 ? 'border-orange-500/20' : 'border-green-500/20' }
188
- ].map((metric, i) => (
189
- <div key={i} className="bg-surface-container-lowest border border-outline-variant p-md rounded-2xl shadow-sm hover:shadow-md transition-all group">
190
- <div className="flex justify-between items-start">
191
- <div>
192
- <p className="text-on-surface-variant font-bold text-[12px] uppercase tracking-wider mb-1">{metric.title}</p>
193
- <h3 className="text-[32px] font-extrabold tracking-tight text-on-surface">{metric.value}</h3>
194
- </div>
195
- <div className={`${metric.bg} ${metric.border} p-2.5 rounded-xl border group-hover:scale-110 transition-transform`}>
196
- <metric.icon className={`${metric.color} w-6 h-6`} />
197
- </div>
198
- </div>
199
- </div>
200
- ))}
201
- </div>
202
-
203
- {/* Scan Engine Distribution */}
204
- {scanTypeChartData.length > 0 && (
205
- <div className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-sm mb-xl">
206
- <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
207
- <h2 className="font-headline-sm font-bold text-on-surface text-[18px] flex items-center gap-2">
208
- <PieChartIcon className="w-5 h-5 text-purple-400" />
209
- Scan Mode Distribution
210
- </h2>
211
- <div className="flex gap-sm">
212
- <select
213
- value={filterOrg}
214
- onChange={e => setFilterOrg(e.target.value)}
215
- className="bg-surface-container border border-outline-variant text-on-surface rounded-lg px-3 py-1.5 text-sm font-bold outline-none"
216
- >
217
- <option value="All">All Organizations</option>
218
- {uniqueOrgs.map(org => <option key={org} value={org}>{org}</option>)}
219
- </select>
220
- <select
221
- value={filterWebsite}
222
- onChange={e => setFilterWebsite(e.target.value)}
223
- className="bg-surface-container border border-outline-variant text-on-surface rounded-lg px-3 py-1.5 text-sm font-bold outline-none"
224
- >
225
- <option value="All">All Websites</option>
226
- {uniqueWebsites.map(web => <option key={web} value={web}>{web.replace(/^https?:\/\//, '')}</option>)}
227
- </select>
228
- </div>
229
- </div>
230
- <div className="h-[250px] w-full">
231
- <ResponsiveContainer width="100%" height="100%">
232
- <PieChart>
233
- <Pie
234
- data={scanTypeChartData}
235
- cx="50%"
236
- cy="50%"
237
- outerRadius={85}
238
- dataKey="value"
239
- label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
240
- >
241
- {scanTypeChartData.map((entry, index) => (
242
- <Cell key={`scan-cell-${index}`} fill={SCAN_TYPE_COLORS[index % SCAN_TYPE_COLORS.length]} />
243
- ))}
244
- </Pie>
245
- <RechartsTooltip contentStyle={{ backgroundColor: '#1F2937', border: '1px solid #374151', borderRadius: '8px', color: '#fff' }} />
246
- </PieChart>
247
- </ResponsiveContainer>
248
- </div>
249
- </div>
250
- )}
251
-
252
- {/* Row 1: Charts Grid */}
253
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-lg mb-xl">
254
- {/* Risk Trend Line Chart */}
255
- <div className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-sm">
256
- <h2 className="font-headline-sm font-bold text-on-surface mb-6 text-[18px] flex items-center gap-2">
257
- <Activity className="w-5 h-5 text-primary" />
258
- Security Score Trend (Recent {riskTrendData.length} Scans)
259
- </h2>
260
- <div className="h-[300px] w-full">
261
- {riskTrendData.length > 0 ? (
262
- <ResponsiveContainer width="100%" height="100%">
263
- <LineChart data={riskTrendData}>
264
- <CartesianGrid strokeDasharray="3 3" stroke="#374151" vertical={false} />
265
- <XAxis dataKey="name" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 11 }} />
266
- <YAxis stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} domain={[0, 100]} />
267
- <RechartsTooltip
268
- contentStyle={{ backgroundColor: '#1F2937', border: '1px solid #374151', borderRadius: '8px', color: '#fff' }}
269
- itemStyle={{ color: '#fff' }}
270
- />
271
- <Line type="monotone" dataKey="securityScore" name="Security Score" stroke="#3B82F6" strokeWidth={3} dot={{ r: 4, fill: '#3B82F6', strokeWidth: 2 }} activeDot={{ r: 6 }} />
272
- </LineChart>
273
- </ResponsiveContainer>
274
- ) : (
275
- <div className="flex h-full items-center justify-center text-on-surface-variant">No scan history available.</div>
276
- )}
277
- </div>
278
- </div>
279
-
280
- {/* Vulnerability Distribution Bar Chart */}
281
- <div className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-sm">
282
- <h2 className="font-headline-sm font-bold text-on-surface mb-6 text-[18px] flex items-center gap-2">
283
- <BarChart3 className="w-5 h-5 text-primary" />
284
- Severity Level Breakdown
285
- </h2>
286
- <div className="h-[300px] w-full">
287
- <ResponsiveContainer width="100%" height="100%">
288
- <BarChart data={vulnerabilityTypes} margin={{ top: 20, right: 30, left: 0, bottom: 5 }}>
289
- <CartesianGrid strokeDasharray="3 3" stroke="#374151" vertical={false} />
290
- <XAxis dataKey="name" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
291
- <YAxis stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
292
- <RechartsTooltip
293
- contentStyle={{ backgroundColor: '#1F2937', border: '1px solid #374151', borderRadius: '8px', color: '#fff' }}
294
- cursor={{ fill: 'rgba(255, 255, 255, 0.05)' }}
295
- />
296
- <Bar dataKey="value" name="Risks" radius={[4, 4, 0, 0]}>
297
- <LabelList dataKey="value" position="top" fill="#9CA3AF" fontSize={12} fontWeight="bold" />
298
- {vulnerabilityTypes.map((entry, index) => (
299
- <Cell key={`cell-${index}`} fill={entry.name.includes('Clean') ? '#10B981' : SEVERITY_COLORS[index % SEVERITY_COLORS.length]} />
300
- ))}
301
- </Bar>
302
- </BarChart>
303
- </ResponsiveContainer>
304
- </div>
305
- </div>
306
- </div>
307
-
308
- {/* Row 2: Secondary Charts Grid */}
309
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-lg mb-xl">
310
- {/* Scan Frequency vs Issues Found */}
311
- <div className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-sm">
312
- <h2 className="font-headline-sm font-bold text-on-surface mb-6 text-[18px] flex items-center gap-2">
313
- <BarChart3 className="w-5 h-5 text-primary" />
314
- Monthly Scans vs Issues Found
315
- </h2>
316
- <div className="h-[300px] w-full">
317
- {scanHistoryChartData.length > 0 ? (
318
- <ResponsiveContainer width="100%" height="100%">
319
- <ComposedChart data={scanHistoryChartData} margin={{ top: 20, right: 30, left: 0, bottom: 5 }}>
320
- <CartesianGrid strokeDasharray="3 3" stroke="#374151" vertical={false} />
321
- <XAxis dataKey="month" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
322
- <YAxis yAxisId="left" orientation="left" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
323
- <YAxis yAxisId="right" orientation="right" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
324
- <RechartsTooltip
325
- contentStyle={{ backgroundColor: '#1F2937', border: '1px solid #374151', borderRadius: '8px', color: '#fff' }}
326
- cursor={{ fill: 'rgba(255, 255, 255, 0.05)' }}
327
- />
328
- <Legend wrapperStyle={{ fontSize: '12px' }} />
329
- <Bar yAxisId="left" dataKey="scans" name="Total Scans" fill="#3B82F6" radius={[4, 4, 0, 0]} maxBarSize={40}>
330
- <LabelList dataKey="scans" position="insideTop" fill="#ffffff" fontSize={11} fontWeight="bold" offset={10} />
331
- </Bar>
332
- <Line yAxisId="right" type="monotone" dataKey="issues" name="Issues Found" stroke="#EF4444" strokeWidth={3} dot={{ r: 4, fill: '#EF4444', strokeWidth: 2 }} activeDot={{ r: 6 }}>
333
- <LabelList dataKey="issues" position="top" fill="#EF4444" fontSize={12} fontWeight="bold" offset={10} />
334
- </Line>
335
- </ComposedChart>
336
- </ResponsiveContainer>
337
- ) : (
338
- <div className="flex h-full items-center justify-center text-on-surface-variant">No scan history available.</div>
339
- )}
340
- </div>
341
- </div>
342
-
343
- {/* Top Vulnerability Categories */}
344
- <div className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-sm">
345
- <h2 className="font-headline-sm font-bold text-on-surface mb-6 text-[18px] flex items-center gap-2">
346
- <ShieldAlert className="w-5 h-5 text-orange-500" />
347
- Top Vulnerability Categories (OWASP)
348
- </h2>
349
- <div className="h-[300px] w-full">
350
- {categoriesData.length > 0 ? (
351
- <ResponsiveContainer width="100%" height="100%">
352
- <BarChart data={categoriesData} layout="vertical" margin={{ top: 10, right: 30, left: 40, bottom: 5 }}>
353
- <CartesianGrid strokeDasharray="3 3" stroke="#374151" horizontal={false} />
354
- <XAxis type="number" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
355
- <YAxis dataKey="category" type="category" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 11 }} width={110} />
356
- <RechartsTooltip
357
- contentStyle={{ backgroundColor: '#1F2937', border: '1px solid #374151', borderRadius: '8px', color: '#fff' }}
358
- />
359
- <Bar dataKey="count" name="Detections" fill="#F97316" radius={[0, 4, 4, 0]}>
360
- <LabelList dataKey="count" position="right" fill="#9CA3AF" fontSize={11} fontWeight="bold" />
361
- </Bar>
362
- </BarChart>
363
- </ResponsiveContainer>
364
- ) : (
365
- <div className="flex h-full items-center justify-center text-on-surface-variant">No category breakdown available.</div>
366
- )}
367
- </div>
368
- </div>
369
- </div>
370
-
371
- </div>
372
- );
373
- };
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { useAuth } from '../components/AuthContext';
3
+ import { Shield, Activity, Users, Globe, Lock, ShieldAlert, ArrowLeft, BarChart3, PieChart as PieChartIcon } from 'lucide-react';
4
+ import { useNavigate } from 'react-router-dom';
5
+ import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell, Legend, LabelList, ComposedChart } from 'recharts';
6
+
7
+ const SEVERITY_COLORS = ['#EF4444', '#F97316', '#EAB308', '#3B82F6']; // Critical, High, Medium, Low
8
+ const SCAN_TYPE_COLORS = ['#3B82F6', '#8B5CF6', '#10B981', '#F59E0B'];
9
+
10
+ const CustomChartTooltip = ({ active, payload, label }) => {
11
+ if (active && payload && payload.length) {
12
+ return (
13
+ <div className="bg-[#0f172a] border border-[#334155] rounded-lg px-3 py-1.5 shadow-xl text-left pointer-events-none">
14
+ {label && <div className="text-[12px] font-bold text-sky-400 leading-tight mb-1">{label}</div>}
15
+ {payload.map((item, index) => (
16
+ <div key={index} className="text-[12px] font-medium text-slate-200 leading-tight">
17
+ <span style={{ color: item.color || '#38bdf8' }}>{item.name || item.dataKey}</span> : <span className="font-bold text-white">{item.value}</span>
18
+ </div>
19
+ ))}
20
+ </div>
21
+ );
22
+ }
23
+ return null;
24
+ };
25
+
26
+ export const OrganizationPage = () => {
27
+ const { user, token } = useAuth();
28
+ const navigate = useNavigate();
29
+ const [loading, setLoading] = useState(true);
30
+
31
+ const [summaryData, setSummaryData] = useState(null);
32
+ const [scanHistory, setScanHistory] = useState([]);
33
+ const [filterOrg, setFilterOrg] = useState('All');
34
+ const [filterWebsite, setFilterWebsite] = useState('All');
35
+
36
+ const getToken = useCallback(() => localStorage.getItem('wss_token') || localStorage.getItem('wss_token') || token, [token]);
37
+
38
+ const fetchData = useCallback(async () => {
39
+ try {
40
+ const activeToken = getToken();
41
+ const [summaryRes, historyRes] = await Promise.all([
42
+ fetch('/api/vulnerabilities/summary?global=true', { headers: { 'Authorization': `Bearer ${activeToken}` } }),
43
+ fetch('/api/scans/history?global=true&limit=100', { headers: { 'Authorization': `Bearer ${activeToken}` } })
44
+ ]);
45
+
46
+ if (!summaryRes.ok || !historyRes.ok) return;
47
+
48
+ const summaryJson = await summaryRes.json();
49
+ const historyJson = await historyRes.json();
50
+
51
+ setSummaryData(summaryJson.summary);
52
+ setScanHistory(historyJson.scans || []);
53
+ } catch (err) {
54
+ console.error("Failed to fetch organization data:", err);
55
+ } finally {
56
+ setLoading(false);
57
+ }
58
+ }, [getToken]);
59
+
60
+ useEffect(() => {
61
+ fetchData();
62
+ const interval = setInterval(fetchData, 5000); // Polling real-time data every 5s
63
+ return () => clearInterval(interval);
64
+ }, [fetchData]);
65
+
66
+ if (loading) {
67
+ return (
68
+ <div className="flex h-[80vh] items-center justify-center">
69
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
70
+ </div>
71
+ );
72
+ }
73
+
74
+ // Extract Unique Orgs & Websites for filters
75
+ const uniqueOrgs = [...new Set(scanHistory.map(s => s.org_name).filter(Boolean))];
76
+ const uniqueWebsites = [...new Set(scanHistory.map(s => s.target_url).filter(Boolean))];
77
+
78
+ // Apply Filters
79
+ const filteredScans = scanHistory
80
+ .filter(scan => filterOrg === 'All' || scan.org_name === filterOrg)
81
+ .filter(scan => filterWebsite === 'All' || scan.target_url === filterWebsite);
82
+
83
+ // Derive metrics dynamically from filteredScans
84
+ const completedScans = filteredScans.filter(s => s.status === 'completed' && s.security_score !== null);
85
+ const score = completedScans.length > 0
86
+ ? Math.round(completedScans.reduce((sum, s) => sum + s.security_score, 0) / completedScans.length)
87
+ : 100;
88
+
89
+ const totalVulnerabilities = filteredScans.reduce((sum, s) => {
90
+ const vc = s.vulnerabilities_count;
91
+ return sum + (vc ? (vc.critical + vc.high + vc.medium + vc.low) : (s.total_vulnerabilities || 0));
92
+ }, 0);
93
+
94
+ // Active Assets (unique target URLs)
95
+ const activeAssets = new Set(filteredScans.map(s => s.target_url).filter(Boolean)).size;
96
+
97
+ // Vulnerability Distribution Pie Chart Data
98
+ const vulnCounts = filteredScans.reduce((acc, s) => {
99
+ if (s.vulnerabilities_count) {
100
+ acc.critical += s.vulnerabilities_count.critical || 0;
101
+ acc.high += s.vulnerabilities_count.high || 0;
102
+ acc.medium += s.vulnerabilities_count.medium || 0;
103
+ acc.low += s.vulnerabilities_count.low || 0;
104
+ }
105
+ return acc;
106
+ }, { critical: 0, high: 0, medium: 0, low: 0 });
107
+
108
+ const vulnerabilityTypes = [
109
+ { name: 'Critical', value: vulnCounts.critical },
110
+ { name: 'High', value: vulnCounts.high },
111
+ { name: 'Medium', value: vulnCounts.medium },
112
+ { name: 'Low', value: vulnCounts.low },
113
+ ].filter(v => v.value > 0);
114
+
115
+ if (vulnerabilityTypes.length === 0) {
116
+ vulnerabilityTypes.push({ name: 'Clean / No Risks', value: 1 });
117
+ }
118
+
119
+ // Risk Score Trend (Last 10 Scans)
120
+ const riskTrendData = [...filteredScans]
121
+ .filter(s => s.started_at)
122
+ .sort((a, b) => new Date(a.started_at) - new Date(b.started_at))
123
+ .slice(-10)
124
+ .map((scan, index) => {
125
+ const totalVulns = scan.vulnerabilities_count
126
+ ? (scan.vulnerabilities_count.critical + scan.vulnerabilities_count.high + scan.vulnerabilities_count.medium + scan.vulnerabilities_count.low)
127
+ : (scan.total_vulnerabilities || 0);
128
+ return {
129
+ name: scan.target_url ? scan.target_url.replace('https://', '').replace('http://', '').replace(/\/$/, '') : `Scan ${index + 1}`,
130
+ securityScore: Math.round(scan.security_score ?? 100),
131
+ vulnerabilities: totalVulns
132
+ };
133
+ });
134
+
135
+ // Group Scans by Month for Bar Chart
136
+ const monthsData = {};
137
+ filteredScans.forEach(scan => {
138
+ const d = new Date(scan.started_at || scan.created_at);
139
+ if (isNaN(d.getTime())) return;
140
+ const month = d.toLocaleString('en-US', { month: 'short', year: 'numeric' });
141
+ if (!monthsData[month]) monthsData[month] = { month, scans: 0, issues: 0 };
142
+ monthsData[month].scans += 1;
143
+ const totalVulns = scan.vulnerabilities_count
144
+ ? (scan.vulnerabilities_count.critical + scan.vulnerabilities_count.high + scan.vulnerabilities_count.medium + scan.vulnerabilities_count.low)
145
+ : (scan.total_vulnerabilities || 0);
146
+ monthsData[month].issues += totalVulns;
147
+ });
148
+ const scanHistoryChartData = Object.values(monthsData);
149
+
150
+ // Top Vulnerability Categories Chart Data
151
+ const categoriesData = Object.entries(summaryData?.by_category || {})
152
+ .map(([cat, count]) => ({ category: cat || 'General Security', count }))
153
+ .sort((a, b) => b.count - a.count)
154
+ .slice(0, 6);
155
+
156
+ // Extract Unique Orgs & Websites for filters is moved up.
157
+
158
+ // Scan Types Breakdown Chart Data (Filtered)
159
+ const scanTypeCounts = {};
160
+ filteredScans.forEach(scan => {
161
+ const type = scan.scan_type ? (scan.scan_type.charAt(0).toUpperCase() + scan.scan_type.slice(1)) + ' Scan' : 'Advanced Scan';
162
+ scanTypeCounts[type] = (scanTypeCounts[type] || 0) + 1;
163
+ });
164
+ const scanTypeChartData = Object.entries(scanTypeCounts).map(([name, value]) => ({ name, value }));
165
+
166
+ return (
167
+ <div className="w-full text-on-surface animate-fade-in pb-xl">
168
+ <div className="flex flex-col md:flex-row md:items-center justify-between mb-xl gap-sm">
169
+ <div>
170
+ <h1 className="text-[28px] font-extrabold font-display tracking-tight brand-gradient flex items-center gap-2">
171
+ <Globe className="w-8 h-8 text-primary" />
172
+ LarShield Global Management
173
+ </h1>
174
+ <p className="text-on-surface-variant text-[14px] mt-1">Centralized oversight for all client organizations, scans, and security nodes.</p>
175
+ </div>
176
+ <div className="flex gap-sm flex-wrap">
177
+ <button onClick={fetchData} className="flex items-center px-md py-sm bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[13.5px] cursor-pointer">
178
+ <Activity className={`w-4 h-4 mr-2 text-primary ${loading ? 'animate-spin' : ''}`} /> Sync Metrics
179
+ </button>
180
+ <button onClick={() => navigate('/super-admin')} className="flex items-center px-md py-sm bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[13.5px] cursor-pointer">
181
+ <Lock className="w-4 h-4 mr-2 text-primary" /> Manage Pricing
182
+ </button>
183
+ <button onClick={() => navigate('/organization')} className="flex items-center px-md py-sm bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[13.5px] cursor-pointer">
184
+ <Activity className="w-4 h-4 mr-2 text-primary" /> Org Dashboard
185
+ </button>
186
+ <button onClick={() => navigate('/super-admin/logs')} className="flex items-center px-md py-sm bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[13.5px] cursor-pointer">
187
+ <ShieldAlert className="w-4 h-4 mr-2 text-primary" /> Logs & Threats
188
+ </button>
189
+ <button onClick={() => navigate(-1)} className="flex items-center px-md py-sm bg-primary text-white rounded-lg hover:brightness-110 transition-all font-bold text-[13.5px] border-0 cursor-pointer shadow-md shadow-primary/20">
190
+ <ArrowLeft className="w-4 h-4 mr-2" /> Back
191
+ </button>
192
+ </div>
193
+ </div>
194
+
195
+
196
+
197
+ {/* Top Metrics Grid */}
198
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-md mb-xl">
199
+ {[
200
+ { title: 'Security Score', value: `${score}/100`, icon: Shield, color: score > 80 ? 'text-green-500' : score > 50 ? 'text-orange-500' : 'text-error', bg: score > 80 ? 'bg-green-500/10' : score > 50 ? 'bg-orange-500/10' : 'bg-error/10', border: score > 80 ? 'border-green-500/20' : score > 50 ? 'border-orange-500/20' : 'border-error/20' },
201
+ { title: 'Active Assets', value: activeAssets.toString(), icon: Globe, color: 'text-blue-500', bg: 'bg-blue-500/10', border: 'border-blue-500/20' },
202
+ { title: 'Total Scans', value: filteredScans.length.toString(), icon: Activity, color: 'text-purple-500', bg: 'bg-purple-500/10', border: 'border-purple-500/20' },
203
+ { title: 'Open Risks', value: totalVulnerabilities.toString(), icon: ShieldAlert, color: totalVulnerabilities > 0 ? 'text-orange-500' : 'text-green-500', bg: totalVulnerabilities > 0 ? 'bg-orange-500/10' : 'bg-green-500/10', border: totalVulnerabilities > 0 ? 'border-orange-500/20' : 'border-green-500/20' }
204
+ ].map((metric, i) => (
205
+ <div key={i} className="bg-surface-container-lowest border border-outline-variant p-md rounded-2xl shadow-sm hover:shadow-md transition-all group">
206
+ <div className="flex justify-between items-start">
207
+ <div>
208
+ <p className="text-on-surface-variant font-bold text-[12px] uppercase tracking-wider mb-1">{metric.title}</p>
209
+ <h3 className="text-[32px] font-extrabold tracking-tight text-on-surface">{metric.value}</h3>
210
+ </div>
211
+ <div className={`${metric.bg} ${metric.border} p-2.5 rounded-xl border group-hover:scale-110 transition-transform`}>
212
+ <metric.icon className={`${metric.color} w-6 h-6`} />
213
+ </div>
214
+ </div>
215
+ </div>
216
+ ))}
217
+ </div>
218
+
219
+ {/* Scan Engine Distribution */}
220
+ {scanTypeChartData.length > 0 && (
221
+ <div className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-sm mb-xl">
222
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
223
+ <h2 className="font-headline-sm font-bold text-on-surface text-[18px] flex items-center gap-2">
224
+ <PieChartIcon className="w-5 h-5 text-purple-400" />
225
+ Scan Mode Distribution
226
+ </h2>
227
+ <div className="flex gap-sm">
228
+ <select
229
+ value={filterOrg}
230
+ onChange={e => setFilterOrg(e.target.value)}
231
+ className="bg-surface-container border border-outline-variant text-on-surface rounded-lg px-3 py-1.5 text-sm font-bold outline-none"
232
+ >
233
+ <option value="All">All Organizations</option>
234
+ {uniqueOrgs.map(org => <option key={org} value={org}>{org}</option>)}
235
+ </select>
236
+ <select
237
+ value={filterWebsite}
238
+ onChange={e => setFilterWebsite(e.target.value)}
239
+ className="bg-surface-container border border-outline-variant text-on-surface rounded-lg px-3 py-1.5 text-sm font-bold outline-none"
240
+ >
241
+ <option value="All">All Websites</option>
242
+ {uniqueWebsites.map(web => <option key={web} value={web}>{web.replace(/^https?:\/\//, '')}</option>)}
243
+ </select>
244
+ </div>
245
+ </div>
246
+ <div className="h-[250px] w-full">
247
+ <ResponsiveContainer width="100%" height="100%">
248
+ <PieChart>
249
+ <Pie
250
+ data={scanTypeChartData}
251
+ cx="50%"
252
+ cy="50%"
253
+ outerRadius={85}
254
+ dataKey="value"
255
+ label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
256
+ >
257
+ {scanTypeChartData.map((entry, index) => (
258
+ <Cell key={`scan-cell-${index}`} fill={SCAN_TYPE_COLORS[index % SCAN_TYPE_COLORS.length]} />
259
+ ))}
260
+ </Pie>
261
+ <RechartsTooltip content={<CustomChartTooltip />} />
262
+ </PieChart>
263
+ </ResponsiveContainer>
264
+ </div>
265
+ </div>
266
+ )}
267
+
268
+ {/* Row 1: Charts Grid */}
269
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-lg mb-xl">
270
+ {/* Risk Trend Line Chart */}
271
+ <div className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-sm">
272
+ <h2 className="font-headline-sm font-bold text-on-surface mb-6 text-[18px] flex items-center gap-2">
273
+ <Activity className="w-5 h-5 text-primary" />
274
+ Security Score Trend (Recent {riskTrendData.length} Scans)
275
+ </h2>
276
+ <div className="h-[300px] w-full">
277
+ {riskTrendData.length > 0 ? (
278
+ <ResponsiveContainer width="100%" height="100%">
279
+ <LineChart data={riskTrendData}>
280
+ <CartesianGrid strokeDasharray="3 3" stroke="#374151" vertical={false} />
281
+ <XAxis dataKey="name" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 11 }} />
282
+ <YAxis stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} domain={[0, 100]} />
283
+ <RechartsTooltip content={<CustomChartTooltip />} />
284
+ <Line type="monotone" dataKey="securityScore" name="Security Score" stroke="#3B82F6" strokeWidth={3} dot={{ r: 4, fill: '#3B82F6', strokeWidth: 2 }} activeDot={{ r: 6 }} />
285
+ </LineChart>
286
+ </ResponsiveContainer>
287
+ ) : (
288
+ <div className="flex h-full items-center justify-center text-on-surface-variant">No scan history available.</div>
289
+ )}
290
+ </div>
291
+ </div>
292
+
293
+ {/* Vulnerability Distribution Bar Chart */}
294
+ <div className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-sm">
295
+ <h2 className="font-headline-sm font-bold text-on-surface mb-6 text-[18px] flex items-center gap-2">
296
+ <BarChart3 className="w-5 h-5 text-primary" />
297
+ Severity Level Breakdown
298
+ </h2>
299
+ <div className="h-[300px] w-full">
300
+ <ResponsiveContainer width="100%" height="100%">
301
+ <BarChart data={vulnerabilityTypes} margin={{ top: 20, right: 30, left: 0, bottom: 5 }}>
302
+ <CartesianGrid strokeDasharray="3 3" stroke="#374151" vertical={false} />
303
+ <XAxis dataKey="name" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
304
+ <YAxis stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
305
+ <RechartsTooltip content={<CustomChartTooltip />} />
306
+ <Bar dataKey="value" name="Risks" radius={[4, 4, 0, 0]}>
307
+ <LabelList dataKey="value" position="top" fill="#9CA3AF" fontSize={12} fontWeight="bold" />
308
+ {vulnerabilityTypes.map((entry, index) => (
309
+ <Cell key={`cell-${index}`} fill={entry.name.includes('Clean') ? '#10B981' : SEVERITY_COLORS[index % SEVERITY_COLORS.length]} />
310
+ ))}
311
+ </Bar>
312
+ </BarChart>
313
+ </ResponsiveContainer>
314
+ </div>
315
+ </div>
316
+ </div>
317
+
318
+ {/* Row 2: Secondary Charts Grid */}
319
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-lg mb-xl">
320
+ {/* Scan Frequency vs Issues Found */}
321
+ <div className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-sm">
322
+ <h2 className="font-headline-sm font-bold text-on-surface mb-6 text-[18px] flex items-center gap-2">
323
+ <BarChart3 className="w-5 h-5 text-primary" />
324
+ Monthly Scans vs Issues Found
325
+ </h2>
326
+ <div className="h-[300px] w-full">
327
+ {scanHistoryChartData.length > 0 ? (
328
+ <ResponsiveContainer width="100%" height="100%">
329
+ <ComposedChart data={scanHistoryChartData} margin={{ top: 20, right: 30, left: 0, bottom: 5 }}>
330
+ <CartesianGrid strokeDasharray="3 3" stroke="#374151" vertical={false} />
331
+ <XAxis dataKey="month" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
332
+ <YAxis yAxisId="left" orientation="left" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
333
+ <YAxis yAxisId="right" orientation="right" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
334
+ <RechartsTooltip content={<CustomChartTooltip />} />
335
+ <Legend wrapperStyle={{ fontSize: '12px' }} />
336
+ <Bar yAxisId="left" dataKey="scans" name="Total Scans" fill="#3B82F6" radius={[4, 4, 0, 0]} maxBarSize={40}>
337
+ <LabelList dataKey="scans" position="insideTop" fill="#ffffff" fontSize={11} fontWeight="bold" offset={10} />
338
+ </Bar>
339
+ <Line yAxisId="right" type="monotone" dataKey="issues" name="Issues Found" stroke="#EF4444" strokeWidth={3} dot={{ r: 4, fill: '#EF4444', strokeWidth: 2 }} activeDot={{ r: 6 }}>
340
+ <LabelList dataKey="issues" position="top" fill="#EF4444" fontSize={12} fontWeight="bold" offset={10} />
341
+ </Line>
342
+ </ComposedChart>
343
+ </ResponsiveContainer>
344
+ ) : (
345
+ <div className="flex h-full items-center justify-center text-on-surface-variant">No scan history available.</div>
346
+ )}
347
+ </div>
348
+ </div>
349
+
350
+ {/* Top Vulnerability Categories */}
351
+ <div className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-sm">
352
+ <h2 className="font-headline-sm font-bold text-on-surface mb-6 text-[18px] flex items-center gap-2">
353
+ <ShieldAlert className="w-5 h-5 text-orange-500" />
354
+ Top Vulnerability Categories (OWASP)
355
+ </h2>
356
+ <div className="h-[300px] w-full">
357
+ {categoriesData.length > 0 ? (
358
+ <ResponsiveContainer width="100%" height="100%">
359
+ <BarChart data={categoriesData} layout="vertical" margin={{ top: 10, right: 30, left: 40, bottom: 5 }}>
360
+ <CartesianGrid strokeDasharray="3 3" stroke="#374151" horizontal={false} />
361
+ <XAxis type="number" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
362
+ <YAxis dataKey="category" type="category" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 11 }} width={110} />
363
+ <RechartsTooltip content={<CustomChartTooltip />} />
364
+ <Bar dataKey="count" name="Detections" fill="#F97316" radius={[0, 4, 4, 0]}>
365
+ <LabelList dataKey="count" position="right" fill="#9CA3AF" fontSize={11} fontWeight="bold" />
366
+ </Bar>
367
+ </BarChart>
368
+ </ResponsiveContainer>
369
+ ) : (
370
+ <div className="flex h-full items-center justify-center text-on-surface-variant">No category breakdown available.</div>
371
+ )}
372
+ </div>
373
+ </div>
374
+ </div>
375
+
376
+ </div>
377
+ );
378
+ };