larxius commited on
Commit
d719482
·
verified ·
1 Parent(s): c4de973

Update frontend/src/pages/NewScan.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/pages/NewScan.jsx +591 -504
frontend/src/pages/NewScan.jsx CHANGED
@@ -1,504 +1,591 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { useNavigate } from 'react-router-dom';
3
- import { useAuth } from '../components/AuthContext';
4
- import toast from 'react-hot-toast';
5
-
6
- export const NewScan = () => {
7
- const [targetUrl, setTargetUrl] = useState('');
8
- const [scanType, setScanType] = useState('Quick');
9
- const [error, setError] = useState('');
10
- const [loading, setLoading] = useState(false);
11
- const [customHeaders, setCustomHeaders] = useState('');
12
- const [crawlDepth, setCrawlDepth] = useState('3');
13
- const [excludePaths, setExcludePaths] = useState('');
14
- const [enableRedTeam, setEnableRedTeam] = useState(false);
15
- const [scanConfig, setScanConfig] = useState([]);
16
- const [showAdvanced, setShowAdvanced] = useState(false);
17
- const [showUpgradeModal, setShowUpgradeModal] = useState(false);
18
- const [attemptedScan, setAttemptedScan] = useState('');
19
- const [quotas, setQuotas] = useState([]);
20
-
21
- const [isScheduled, setIsScheduled] = useState(false);
22
- const [scheduleFrequency, setScheduleFrequency] = useState('daily');
23
- const [scheduleTime, setScheduleTime] = useState('02:00');
24
-
25
- const { token, user } = useAuth();
26
- const navigate = useNavigate();
27
-
28
- useEffect(() => {
29
- fetch('/api/scans/config')
30
- .then(res => res.json())
31
- .then(data => {
32
- if (data.config) {
33
- setScanConfig(data.config);
34
- }
35
- })
36
- .catch(err => console.error("Failed to fetch scan config", err));
37
- }, []);
38
-
39
- useEffect(() => {
40
- if (user?.org_id && token) {
41
- fetch(`/api/auth/organizations/${user.org_id}/quotas`, {
42
- headers: { 'Authorization': `Bearer ${token}` }
43
- })
44
- .then(res => res.json())
45
- .then(data => {
46
- // The backend returns an array directly if it's the result variable,
47
- // wait, let's log or safely set it. I will set it to data if data is an array, else data.quotas
48
- if (Array.isArray(data)) {
49
- setQuotas(data);
50
- } else if (data.quotas) {
51
- setQuotas(data.quotas);
52
- }
53
- })
54
- .catch(console.error);
55
- }
56
- }, [user?.org_id, token]);
57
-
58
- useEffect(() => {
59
- if (scanType === 'Deep') {
60
- setEnableRedTeam(true);
61
- setCrawlDepth('20');
62
- } else if (scanType === 'Advanced') {
63
- setEnableRedTeam(true);
64
- setCrawlDepth('10');
65
- } else if (scanType === 'Quick') {
66
- setEnableRedTeam(false);
67
- setCrawlDepth('3');
68
- }
69
- }, [scanType]);
70
-
71
- const handleLaunch = async (e) => {
72
- e.preventDefault();
73
- setError('');
74
-
75
- if (!targetUrl) {
76
- setError('Please provide a target host URL.');
77
- return;
78
- }
79
-
80
- try {
81
- const urlObj = new URL(targetUrl);
82
- if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
83
- setError('Please enter a valid website URL (must start with http:// or https://)');
84
- return;
85
- }
86
- if (!urlObj.hostname.includes('.')) {
87
- setError('Please enter a valid website URL (must have a valid domain structure)');
88
- return;
89
- }
90
- } catch (_) {
91
- setError('Please enter a valid website URL (must start with http:// or https://)');
92
- return;
93
- }
94
-
95
- setLoading(true);
96
-
97
- try {
98
- let parsedAuthHeaders = {};
99
- if (customHeaders) {
100
- const lines = customHeaders.split('\n');
101
- lines.forEach(line => {
102
- const parts = line.split(':');
103
- if (parts.length >= 2) {
104
- const key = parts[0].trim();
105
- const value = parts.slice(1).join(':').trim();
106
- if (key && value) parsedAuthHeaders[key] = value;
107
- }
108
- });
109
- }
110
-
111
- if (isScheduled) {
112
- const res = await fetch('/api/scans/schedule', {
113
- method: 'POST',
114
- headers: {
115
- 'Content-Type': 'application/json',
116
- 'Authorization': `Bearer ${token}`
117
- },
118
- body: JSON.stringify({
119
- target_url: targetUrl,
120
- scan_type: scanType,
121
- frequency: scheduleFrequency,
122
- schedule_time: scheduleTime
123
- })
124
- });
125
- const data = await res.json();
126
- if (res.ok) {
127
- toast.success('Scan scheduled successfully!');
128
- navigate('/dashboard');
129
- } else {
130
- toast.error(data.message || 'Failed to schedule scan.');
131
- }
132
- } else {
133
- const res = await fetch('/api/scans/new', {
134
- method: 'POST',
135
- headers: {
136
- 'Content-Type': 'application/json',
137
- 'Authorization': `Bearer ${token}`
138
- },
139
- body: JSON.stringify({
140
- target_url: targetUrl,
141
- scan_type: scanType,
142
- auth_headers: parsedAuthHeaders,
143
- custom_headers: customHeaders,
144
- crawl_depth: crawlDepth,
145
- exclude_paths: excludePaths,
146
- enable_red_team: enableRedTeam
147
- })
148
- });
149
-
150
- const data = await res.json();
151
- if (res.ok) {
152
- navigate('/dashboard');
153
- } else {
154
- setError(data.message || 'Failed to initialize vulnerability scanning thread.');
155
- }
156
- }
157
- } catch (err) {
158
- setError('Connection timeout. Scanner microservice unavailable.');
159
- console.error(err);
160
- } finally {
161
- setLoading(false);
162
- }
163
- };
164
-
165
- const scanMethodologies = [
166
- {
167
- id: 'Quick',
168
- title: 'Quick Scan',
169
- icon: 'bolt',
170
- desc: 'Rapid recon: HTTP headers audit, Nmap top-100 ports, SSLyze TLS check, technology fingerprinting & DNS lookup.',
171
- tools: ['Nmap', 'SSLyze', 'Headers', 'WHOIS'],
172
- duration: '~2-5 mins',
173
- price: '$4.99',
174
- colorClass: 'text-primary',
175
- requiredTier: 'free'
176
- },
177
- {
178
- id: 'Advanced',
179
- title: 'Advanced Scan',
180
- icon: 'security',
181
- desc: 'Comprehensive deep crawl: All security modules, XSS/SQLi fuzzing, path traversal, Nuclei templates & OWASP ZAP passive analysis.',
182
- tools: ['Nuclei', 'ZAP Passive', 'Fuzzer', 'Dir Scan', 'Subfinder', 'Amass'],
183
- duration: '~20-40 mins',
184
- price: '$44.99',
185
- colorClass: 'text-primary',
186
- requiredTier: 'pro'
187
- },
188
- {
189
- id: 'Deep',
190
- title: 'Deep Scan',
191
- icon: 'radar',
192
- desc: 'Exhaustive audit: All-port Nmap with vuln scripts, full TLS audit, OWASP ZAP active spider + active attack simulation.',
193
- tools: ['Nmap Full', 'ZAP Active', 'NSE Scripts', 'All Modules'],
194
- duration: '~1 hour+',
195
- price: '$99.99',
196
- colorClass: 'text-primary',
197
- requiredTier: 'enterprise'
198
- }
199
- ];
200
-
201
- const getTierLevel = (tier) => {
202
- if (tier === 'enterprise') return 3;
203
- if (tier === 'pro') return 2;
204
- return 1;
205
- };
206
-
207
- const userTierLevel = getTierLevel(user?.subscription_tier || 'free');
208
-
209
- const hasQuota = (methodId) => {
210
- const q = quotas.find(q => q.scan_type.toLowerCase() === methodId.toLowerCase());
211
- return q && (q.allocated_count > -1 && q.allocated_count - q.used_count > 0);
212
- };
213
-
214
- return (
215
- <div className="max-w-4xl mx-auto w-full flex flex-col gap-lg text-left">
216
-
217
- {/* Page Header */}
218
- <header className="flex flex-col gap-base border-b border-outline-variant pb-md">
219
- <h1 className="font-headline-lg text-headline-lg text-on-surface">Initiate Scan</h1>
220
- <p className="font-body-md text-body-md text-on-surface-variant">
221
- Configure target parameters and execution methodology for a new vulnerability assessment.
222
- </p>
223
- </header>
224
-
225
- {/* Configuration Form Card */}
226
- <form onSubmit={handleLaunch} className="bg-surface-container-lowest border border-outline-variant rounded-xl p-lg flex flex-col gap-xl shadow-sm">
227
-
228
- {/* Error Alert Display */}
229
- {error && (
230
- <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">
231
- <span className="material-symbols-outlined shrink-0">error</span>
232
- <div>{error}</div>
233
- </div>
234
- )}
235
-
236
- {/* Target Configuration */}
237
- <div className="flex flex-col gap-sm">
238
- <label className="font-label-sm text-label-sm text-on-surface uppercase tracking-widest flex items-center gap-xs" htmlFor="target-url">
239
- <span className="material-symbols-outlined text-[16px]">language</span>
240
- Target Selection
241
- </label>
242
- <div className="relative flex items-center">
243
- <span className="absolute left-md text-on-surface-variant material-symbols-outlined pointer-events-none text-[20px]">link</span>
244
- <input
245
- id="target-url"
246
- name="target-url"
247
- type="url"
248
- required
249
- className="w-full bg-surface-container-low border border-outline-variant rounded-lg py-md pl-12 pr-md font-label-md text-label-md text-on-surface placeholder:text-outline focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all shadow-sm"
250
- placeholder="https://app.example.com"
251
- value={targetUrl}
252
- onChange={(e) => setTargetUrl(e.target.value)}
253
- />
254
- </div>
255
- <p className="font-body-sm text-body-sm text-on-surface-variant mt-xs">
256
- Ensure you have authorization to scan the specified domain or IP address.
257
- </p>
258
- </div>
259
- <hr className="border-outline-variant border-t" />
260
-
261
- {/* Scan Methodology Section */}
262
- <div className="flex flex-col gap-md">
263
- <div className="flex flex-col gap-xs">
264
- <label className="font-label-sm text-label-sm text-on-surface uppercase tracking-widest flex items-center gap-xs">
265
- <span className="material-symbols-outlined text-[16px]">tune</span>
266
- Scan Methodology
267
- </label>
268
- <p className="font-body-sm text-body-sm text-on-surface-variant">
269
- Purchasing a plan provides <strong>3 full scans</strong> for your target website property.
270
- </p>
271
- </div>
272
- <div className="grid grid-cols-1 md:grid-cols-3 gap-md">
273
- {scanMethodologies.map((method) => {
274
- const config = scanConfig.find(c => c.scan_type === method.id);
275
- const requiredTier = config ? config.required_tier : method.requiredTier;
276
- const isEnabled = config ? config.is_enabled : true;
277
-
278
- const isSelected = scanType === method.id;
279
- const isLocked = userTierLevel < getTierLevel(requiredTier) && !hasQuota(method.id);
280
-
281
- if (!isEnabled) {
282
- return (
283
- <div key={method.id} className="border border-outline-variant bg-surface-container-highest/20 rounded-lg p-md flex flex-col gap-sm relative opacity-50 cursor-not-allowed">
284
- <div className="flex justify-between items-start">
285
- <div className="h-10 w-10 rounded-full flex items-center justify-center bg-surface-container-high text-outline">
286
- <span className="material-symbols-outlined">block</span>
287
- </div>
288
- <span className="text-[10px] font-bold uppercase tracking-wider px-2 py-1 bg-surface-container-high text-on-surface-variant rounded-md">Disabled</span>
289
- </div>
290
- <div className="flex flex-col gap-xs mt-sm text-left">
291
- <span className="font-label-md text-label-md text-on-surface font-bold">{method.title}</span>
292
- <span className="font-body-sm text-body-sm text-on-surface-variant line-clamp-3">Currently unavailable.</span>
293
- </div>
294
- </div>
295
- );
296
- }
297
-
298
- return (
299
- <div
300
- key={method.id}
301
- onClick={() => {
302
- if (isLocked) {
303
- setAttemptedScan(method.title);
304
- setShowUpgradeModal(true);
305
- return;
306
- }
307
- setScanType(method.id);
308
- }}
309
- className={`border rounded-lg p-md cursor-pointer transition-all flex flex-col gap-sm relative group ${
310
- isLocked ? 'border-outline-variant bg-surface-container/50 hover:bg-surface-container opacity-70' :
311
- isSelected
312
- ? 'border-primary bg-primary/5 shadow-[0_0_0_1px_#2563eb]'
313
- : 'border-outline-variant bg-surface-container-lowest hover:bg-surface-container-low'
314
- }`}
315
- >
316
- <div className="flex justify-between items-start">
317
- <div className={`h-10 w-10 rounded-full flex items-center justify-center transition-colors ${
318
- isLocked ? 'bg-surface-container-high text-outline' :
319
- isSelected
320
- ? 'bg-primary/10 text-primary'
321
- : 'bg-surface-container text-secondary group-hover:bg-surface-container-high'
322
- }`}>
323
- <span className="material-symbols-outlined">{isLocked ? 'lock' : method.icon}</span>
324
- </div>
325
-
326
- {!isLocked && (
327
- <span className={`material-symbols-outlined transition-all ${isSelected
328
- ? 'opacity-100 text-primary'
329
- : 'opacity-0 text-outline'
330
- }`} style={{ fontVariationSettings: isSelected ? "'FILL' 1" : "normal" }}>
331
- check_circle
332
- </span>
333
- )}
334
- {isLocked && (
335
- <span className="text-[10px] font-bold uppercase tracking-wider px-2 py-1 bg-surface-container-high text-on-surface-variant rounded-md">
336
- {requiredTier}
337
- </span>
338
- )}
339
- </div>
340
-
341
- <div className="flex flex-col gap-xs mt-sm text-left">
342
- <div className="flex items-center justify-between">
343
- <span className="font-label-md text-label-md text-on-surface font-bold">
344
- {method.title}
345
- </span>
346
- <span className="text-[11px] font-extrabold px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20">
347
- {method.price}
348
- </span>
349
- </div>
350
- <span className="font-body-sm text-body-sm text-on-surface-variant line-clamp-3">
351
- {method.desc}
352
- </span>
353
- {method.tools && (
354
- <div className="flex flex-wrap gap-xs mt-xs">
355
- {method.tools.map(tool => (
356
- <span key={tool} className={`text-[10px] font-bold uppercase tracking-wider px-[6px] py-[2px] rounded-full border ${
357
- isLocked ? 'border-outline-variant text-outline bg-transparent' :
358
- isSelected
359
- ? 'border-primary/40 text-primary bg-primary/10'
360
- : 'border-outline-variant text-on-surface-variant bg-surface-container'
361
- }`}>
362
- {tool}
363
- </span>
364
- ))}
365
- </div>
366
- )}
367
- </div>
368
-
369
- <div className="flex items-center justify-between mt-auto pt-sm">
370
- <span className={`font-label-sm text-label-sm ${
371
- isLocked ? 'text-outline font-bold' :
372
- isSelected ? 'text-primary font-bold' : 'text-secondary'
373
- }`}>
374
- ⏱ {method.duration}
375
- </span>
376
- <span className="text-[11px] font-semibold text-on-surface-variant">
377
- {method.id === 'Quick' ? '13 modules' : method.id === 'Advanced' ? '36 modules' : '89 modules'}
378
- </span>
379
- </div>
380
- </div>
381
- );
382
- })}
383
- </div>
384
- </div>
385
-
386
- <hr className="border-outline-variant border-t" />
387
-
388
- {/* Schedule Scan Section */}
389
- <div className="flex flex-col gap-sm">
390
- <label className="flex items-center gap-sm cursor-pointer font-body-sm text-on-surface">
391
- <input
392
- type="checkbox"
393
- checked={isScheduled}
394
- onChange={(e) => setIsScheduled(e.target.checked)}
395
- className="h-4 w-4 rounded border-outline-variant text-primary focus:ring-primary/30"
396
- />
397
- <span className="font-semibold uppercase tracking-wider font-label-sm">Schedule Automated Scans</span>
398
- <span className="text-on-surface-variant text-[12px]">— Setup recurring scans (Enterprise feature)</span>
399
- </label>
400
-
401
- {isScheduled && (
402
- <div className="grid grid-cols-1 md:grid-cols-2 gap-md p-md bg-surface-container-low dark:bg-inverse-surface rounded-lg mt-xs border border-outline-variant/60">
403
- <div className="flex flex-col gap-xs">
404
- <label className="font-label-sm text-label-sm text-on-surface-variant uppercase tracking-wider font-semibold">Frequency</label>
405
- <select
406
- className="w-full bg-surface-container-lowest border border-outline-variant rounded px-md py-sm font-body-sm text-on-surface focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all cursor-pointer"
407
- value={scheduleFrequency}
408
- onChange={(e) => setScheduleFrequency(e.target.value)}
409
- >
410
- <option value="daily">Daily</option>
411
- <option value="weekly">Weekly (Sundays)</option>
412
- </select>
413
- </div>
414
- <div className="flex flex-col gap-xs">
415
- <label className="font-label-sm text-label-sm text-on-surface-variant uppercase tracking-wider font-semibold">Time (UTC)</label>
416
- <input
417
- type="time"
418
- className="w-full bg-surface-container-lowest border border-outline-variant rounded px-md py-sm font-body-sm text-on-surface focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all"
419
- value={scheduleTime}
420
- onChange={(e) => setScheduleTime(e.target.value)}
421
- />
422
- </div>
423
- </div>
424
- )}
425
- </div>
426
-
427
- <hr className="border-outline-variant border-t" />
428
-
429
- {/* Legal Warning Notice (Bento style block from previous and template combined) */}
430
- <div className="bg-surface-container-low dark:bg-inverse-surface border-l-4 border-primary p-md rounded-lg text-body-sm font-body-sm text-on-surface-variant leading-relaxed">
431
- <strong className="text-on-surface font-semibold uppercase tracking-wider block mb-[4px]">
432
- Operator Notice & Policy Compliance
433
- </strong>
434
- Conducting vulnerability analysis scans against networks or hosts without explicit, verified written authorization is illegal. By executing this scan, you certify that you possess the necessary regulatory clearance to target this host.
435
- </div>
436
-
437
- {/* Action Area */}
438
- <div className="flex justify-end pt-sm border-t border-outline-variant/50">
439
- <button
440
- type="submit"
441
- disabled={loading}
442
- className="bg-primary text-on-primary font-label-md text-label-md px-xl py-md rounded-lg flex items-center gap-sm hover:opacity-90 transition-opacity shadow-sm font-bold border-0 cursor-pointer"
443
- >
444
- {loading ? (
445
- <>
446
- <span className="material-symbols-outlined animate-spin text-[18px]">sync</span>
447
- {isScheduled ? "Scheduling..." : "Executing Pipeline..."}
448
- </>
449
- ) : (
450
- <>
451
- <span className="material-symbols-outlined text-[18px]" style={{ fontVariationSettings: "'FILL' 1" }}>
452
- {isScheduled ? "calendar_month" : "play_arrow"}
453
- </span>
454
- {isScheduled ? "Schedule Automation" : "Execute Scan Pipeline"}
455
- </>
456
- )}
457
- </button>
458
- </div>
459
- </form>
460
-
461
-
462
- {/* Upgrade Modal */}
463
- {showUpgradeModal && (
464
- <div className="fixed inset-0 z-50 flex items-center justify-center p-md">
465
- <div className="absolute inset-0 bg-scrim/50 backdrop-blur-sm" onClick={() => setShowUpgradeModal(false)}></div>
466
- <div className="relative bg-surface-container border border-outline-variant rounded-xl shadow-lg max-w-md w-full flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-200">
467
- <div className="bg-surface-container-highest p-md border-b border-outline-variant flex justify-between items-center">
468
- <h3 className="font-headline-sm text-headline-sm text-on-surface flex items-center gap-sm">
469
- <span className="material-symbols-outlined text-primary">lock</span>
470
- Subscription Required
471
- </h3>
472
- <button onClick={() => setShowUpgradeModal(false)} className="text-on-surface-variant hover:text-on-surface transition-colors cursor-pointer bg-transparent border-0">
473
- <span className="material-symbols-outlined">close</span>
474
- </button>
475
- </div>
476
- <div className="p-xl flex flex-col gap-md text-left">
477
- <p className="font-body-md text-body-md text-on-surface-variant">
478
- You cannot use the <strong className="text-on-surface">{attemptedScan}</strong> methodology on your current plan.
479
- </p>
480
- <p className="font-body-md text-body-md text-on-surface-variant">
481
- If you need to use this feature, please upgrade your subscription plan to unlock advanced vulnerability scanning capabilities.
482
- </p>
483
- </div>
484
- <div className="p-md bg-surface-container-low border-t border-outline-variant flex justify-end gap-sm">
485
- <button
486
- onClick={() => setShowUpgradeModal(false)}
487
- className="px-md py-sm rounded-lg font-label-md text-label-md text-on-surface hover:bg-surface-container-highest transition-colors cursor-pointer border border-outline-variant bg-transparent"
488
- >
489
- Cancel
490
- </button>
491
- <button
492
- onClick={() => navigate('/pricing')}
493
- className="px-md py-sm rounded-lg font-label-md text-label-md bg-primary text-on-primary hover:opacity-90 transition-opacity cursor-pointer border-0 shadow-sm flex items-center gap-xs"
494
- >
495
- View Plans
496
- <span className="material-symbols-outlined text-[18px]">arrow_forward</span>
497
- </button>
498
- </div>
499
- </div>
500
- </div>
501
- )}
502
- </div>
503
- );
504
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { useAuth } from '../components/AuthContext';
4
+ import toast from 'react-hot-toast';
5
+
6
+ export const NewScan = () => {
7
+ const [targetUrl, setTargetUrl] = useState('');
8
+ const [scanType, setScanType] = useState('Quick');
9
+ const [error, setError] = useState('');
10
+ const [loading, setLoading] = useState(false);
11
+ const [customHeaders, setCustomHeaders] = useState('');
12
+ const [crawlDepth, setCrawlDepth] = useState('3');
13
+ const [excludePaths, setExcludePaths] = useState('');
14
+ const [enableRedTeam, setEnableRedTeam] = useState(false);
15
+ const [scanConfig, setScanConfig] = useState([]);
16
+ const [showAdvanced, setShowAdvanced] = useState(false);
17
+ const [showUpgradeModal, setShowUpgradeModal] = useState(false);
18
+ const [showQuotaExceededModal, setShowQuotaExceededModal] = useState(false);
19
+ const [attemptedScan, setAttemptedScan] = useState('');
20
+ const [quotas, setQuotas] = useState([]);
21
+
22
+ const [isScheduled, setIsScheduled] = useState(false);
23
+ const [scheduleFrequency, setScheduleFrequency] = useState('daily');
24
+ const [scheduleTime, setScheduleTime] = useState('02:00');
25
+
26
+ const { token, user } = useAuth();
27
+ const navigate = useNavigate();
28
+
29
+ useEffect(() => {
30
+ fetch('/api/scans/config')
31
+ .then(res => res.json())
32
+ .then(data => {
33
+ if (data.config) {
34
+ setScanConfig(data.config);
35
+ }
36
+ })
37
+ .catch(err => console.error("Failed to fetch scan config", err));
38
+ }, []);
39
+
40
+ useEffect(() => {
41
+ if (user?.org_id && token) {
42
+ fetch(`/api/auth/organizations/${user.org_id}/quotas`, {
43
+ headers: { 'Authorization': `Bearer ${token}` }
44
+ })
45
+ .then(res => res.json())
46
+ .then(data => {
47
+ if (Array.isArray(data)) {
48
+ setQuotas(data);
49
+ } else if (data.quotas) {
50
+ setQuotas(data.quotas);
51
+ }
52
+ })
53
+ .catch(console.error);
54
+ }
55
+ }, [user?.org_id, token]);
56
+
57
+ useEffect(() => {
58
+ if (scanType === 'Deep') {
59
+ setEnableRedTeam(true);
60
+ setCrawlDepth('20');
61
+ } else if (scanType === 'Advanced') {
62
+ setEnableRedTeam(true);
63
+ setCrawlDepth('10');
64
+ } else if (scanType === 'Quick') {
65
+ setEnableRedTeam(false);
66
+ setCrawlDepth('3');
67
+ }
68
+ }, [scanType]);
69
+
70
+ const hasQuota = (methodId) => {
71
+ if (!quotas || quotas.length === 0) return true;
72
+ const q = quotas.find(q => q.scan_type.toLowerCase() === methodId.toLowerCase());
73
+ if (!q) return true;
74
+ if (q.allocated_count === -1) return true;
75
+ return (q.allocated_count - q.used_count) > 0;
76
+ };
77
+
78
+ const handleLaunch = async (e) => {
79
+ e.preventDefault();
80
+ setError('');
81
+
82
+ if (!hasQuota(scanType)) {
83
+ setAttemptedScan(scanType);
84
+ setShowQuotaExceededModal(true);
85
+ return;
86
+ }
87
+
88
+ if (!targetUrl) {
89
+ setError('Please provide a target host URL.');
90
+ return;
91
+ }
92
+
93
+ try {
94
+ const urlObj = new URL(targetUrl);
95
+ if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
96
+ setError('Please enter a valid website URL (must start with http:// or https://)');
97
+ return;
98
+ }
99
+ if (!urlObj.hostname.includes('.')) {
100
+ setError('Please enter a valid website URL (must have a valid domain structure)');
101
+ return;
102
+ }
103
+ } catch (_) {
104
+ setError('Please enter a valid website URL (must start with http:// or https://)');
105
+ return;
106
+ }
107
+
108
+ setLoading(true);
109
+
110
+ try {
111
+ let parsedAuthHeaders = {};
112
+ if (customHeaders) {
113
+ const lines = customHeaders.split('\n');
114
+ lines.forEach(line => {
115
+ const parts = line.split(':');
116
+ if (parts.length >= 2) {
117
+ const key = parts[0].trim();
118
+ const value = parts.slice(1).join(':').trim();
119
+ if (key && value) parsedAuthHeaders[key] = value;
120
+ }
121
+ });
122
+ }
123
+
124
+ if (isScheduled) {
125
+ const res = await fetch('/api/scans/schedule', {
126
+ method: 'POST',
127
+ headers: {
128
+ 'Content-Type': 'application/json',
129
+ 'Authorization': `Bearer ${token}`
130
+ },
131
+ body: JSON.stringify({
132
+ target_url: targetUrl,
133
+ scan_type: scanType,
134
+ frequency: scheduleFrequency,
135
+ schedule_time: scheduleTime
136
+ })
137
+ });
138
+ const data = await res.json();
139
+ if (res.ok) {
140
+ toast.success('Scan scheduled successfully!');
141
+ navigate('/dashboard');
142
+ } else {
143
+ if (res.status === 403 || res.status === 402 || data.message?.toLowerCase().includes('quota')) {
144
+ setAttemptedScan(scanType);
145
+ setShowQuotaExceededModal(true);
146
+ } else {
147
+ toast.error(data.message || 'Failed to schedule scan.');
148
+ }
149
+ }
150
+ } else {
151
+ const res = await fetch('/api/scans/new', {
152
+ method: 'POST',
153
+ headers: {
154
+ 'Content-Type': 'application/json',
155
+ 'Authorization': `Bearer ${token}`
156
+ },
157
+ body: JSON.stringify({
158
+ target_url: targetUrl,
159
+ scan_type: scanType,
160
+ auth_headers: parsedAuthHeaders,
161
+ custom_headers: customHeaders,
162
+ crawl_depth: crawlDepth,
163
+ exclude_paths: excludePaths,
164
+ enable_red_team: enableRedTeam
165
+ })
166
+ });
167
+
168
+ const data = await res.json();
169
+ if (res.ok) {
170
+ toast.success('Scan pipeline initiated successfully!');
171
+ navigate('/dashboard');
172
+ } else {
173
+ if (res.status === 403 || res.status === 402 || data.message?.toLowerCase().includes('quota')) {
174
+ setAttemptedScan(scanType);
175
+ setShowQuotaExceededModal(true);
176
+ } else {
177
+ setError(data.message || 'Failed to initialize vulnerability scanning thread.');
178
+ }
179
+ }
180
+ }
181
+ } catch (err) {
182
+ setError('Connection timeout. Scanner microservice unavailable.');
183
+ console.error(err);
184
+ } finally {
185
+ setLoading(false);
186
+ }
187
+ };
188
+
189
+ const scanMethodologies = [
190
+ {
191
+ id: 'Quick',
192
+ title: 'Quick Scan',
193
+ icon: 'bolt',
194
+ desc: 'Rapid recon: HTTP headers audit, Nmap top-100 ports, SSLyze TLS check, technology fingerprinting & DNS lookup.',
195
+ tools: ['Nmap', 'SSLyze', 'Headers', 'WHOIS'],
196
+ duration: '~2-5 mins',
197
+ price: '$4.99',
198
+ colorClass: 'text-primary',
199
+ requiredTier: 'free'
200
+ },
201
+ {
202
+ id: 'Advanced',
203
+ title: 'Advanced Scan',
204
+ icon: 'security',
205
+ desc: 'Comprehensive deep crawl: All security modules, XSS/SQLi fuzzing, path traversal, Nuclei templates & OWASP ZAP passive analysis.',
206
+ tools: ['Nuclei', 'ZAP Passive', 'Fuzzer', 'Dir Scan', 'Subfinder', 'Amass'],
207
+ duration: '~20-40 mins',
208
+ price: '$44.99',
209
+ colorClass: 'text-primary',
210
+ requiredTier: 'pro'
211
+ },
212
+ {
213
+ id: 'Deep',
214
+ title: 'Deep Scan',
215
+ icon: 'radar',
216
+ desc: 'Exhaustive audit: All-port Nmap with vuln scripts, full TLS audit, OWASP ZAP active spider + active attack simulation.',
217
+ tools: ['Nmap Full', 'ZAP Active', 'NSE Scripts', 'All Modules'],
218
+ duration: '~1 hour+',
219
+ price: '$99.99',
220
+ colorClass: 'text-primary',
221
+ requiredTier: 'enterprise'
222
+ }
223
+ ];
224
+
225
+ const getTierLevel = (tier) => {
226
+ if (tier === 'enterprise') return 3;
227
+ if (tier === 'pro') return 2;
228
+ return 1;
229
+ };
230
+
231
+ const userTierLevel = getTierLevel(user?.subscription_tier || 'free');
232
+
233
+ return (
234
+ <div className="max-w-4xl mx-auto w-full flex flex-col gap-lg text-left">
235
+
236
+ {/* Page Header */}
237
+ <header className="flex flex-col gap-base border-b border-outline-variant pb-md">
238
+ <h1 className="font-headline-lg text-headline-lg text-on-surface">Initiate Scan</h1>
239
+ <p className="font-body-md text-body-md text-on-surface-variant">
240
+ Configure target parameters and execution methodology for a new vulnerability assessment.
241
+ </p>
242
+ </header>
243
+
244
+ {/* Configuration Form Card */}
245
+ <form onSubmit={handleLaunch} className="bg-surface-container-lowest border border-outline-variant rounded-xl p-lg flex flex-col gap-xl shadow-sm">
246
+
247
+ {/* Error Alert Display */}
248
+ {error && (
249
+ <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">
250
+ <span className="material-symbols-outlined shrink-0">error</span>
251
+ <div>{error}</div>
252
+ </div>
253
+ )}
254
+
255
+ {/* Target Configuration */}
256
+ <div className="flex flex-col gap-sm">
257
+ <label className="font-label-sm text-label-sm text-on-surface uppercase tracking-widest flex items-center gap-xs" htmlFor="target-url">
258
+ <span className="material-symbols-outlined text-[16px]">language</span>
259
+ Target Selection
260
+ </label>
261
+ <div className="relative flex items-center">
262
+ <span className="absolute left-md text-on-surface-variant material-symbols-outlined pointer-events-none text-[20px]">link</span>
263
+ <input
264
+ id="target-url"
265
+ name="target-url"
266
+ type="url"
267
+ required
268
+ className="w-full bg-surface-container-low border border-outline-variant rounded-lg py-md pl-12 pr-md font-label-md text-label-md text-on-surface placeholder:text-outline focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all shadow-sm"
269
+ placeholder="https://app.example.com"
270
+ value={targetUrl}
271
+ onChange={(e) => setTargetUrl(e.target.value)}
272
+ />
273
+ </div>
274
+ <p className="font-body-sm text-body-sm text-on-surface-variant mt-xs">
275
+ Ensure you have authorization to scan the specified domain or IP address.
276
+ </p>
277
+ </div>
278
+ <hr className="border-outline-variant border-t" />
279
+
280
+ {/* Scan Methodology Section */}
281
+ <div className="flex flex-col gap-md">
282
+ <div className="flex flex-col gap-xs">
283
+ <label className="font-label-sm text-label-sm text-on-surface uppercase tracking-widest flex items-center gap-xs">
284
+ <span className="material-symbols-outlined text-[16px]">tune</span>
285
+ Scan Methodology
286
+ </label>
287
+ <p className="font-body-sm text-body-sm text-on-surface-variant">
288
+ Select your scanning depth profile. Standard tiers include allocated vulnerability audit quotas.
289
+ </p>
290
+ </div>
291
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-md">
292
+ {scanMethodologies.map((method) => {
293
+ const config = scanConfig.find(c => c.scan_type === method.id);
294
+ const requiredTier = config ? config.required_tier : method.requiredTier;
295
+ const isEnabled = config ? config.is_enabled : true;
296
+
297
+ const isSelected = scanType === method.id;
298
+ const isTierLocked = userTierLevel < getTierLevel(requiredTier);
299
+ const isQuotaExceeded = !hasQuota(method.id);
300
+ const isLocked = isTierLocked || isQuotaExceeded;
301
+
302
+ if (!isEnabled) {
303
+ return (
304
+ <div key={method.id} className="border border-outline-variant bg-surface-container-highest/20 rounded-lg p-md flex flex-col gap-sm relative opacity-50 cursor-not-allowed">
305
+ <div className="flex justify-between items-start">
306
+ <div className="h-10 w-10 rounded-full flex items-center justify-center bg-surface-container-high text-outline">
307
+ <span className="material-symbols-outlined">block</span>
308
+ </div>
309
+ <span className="text-[10px] font-bold uppercase tracking-wider px-2 py-1 bg-surface-container-high text-on-surface-variant rounded-md">Disabled</span>
310
+ </div>
311
+ <div className="flex flex-col gap-xs mt-sm text-left">
312
+ <span className="font-label-md text-label-md text-on-surface font-bold">{method.title}</span>
313
+ <span className="font-body-sm text-body-sm text-on-surface-variant line-clamp-3">Currently unavailable.</span>
314
+ </div>
315
+ </div>
316
+ );
317
+ }
318
+
319
+ return (
320
+ <div
321
+ key={method.id}
322
+ onClick={() => {
323
+ if (isTierLocked) {
324
+ setAttemptedScan(method.title);
325
+ setShowUpgradeModal(true);
326
+ return;
327
+ }
328
+ if (isQuotaExceeded) {
329
+ setAttemptedScan(method.title);
330
+ setShowQuotaExceededModal(true);
331
+ return;
332
+ }
333
+ setScanType(method.id);
334
+ }}
335
+ className={`border rounded-lg p-md cursor-pointer transition-all flex flex-col gap-sm relative group ${
336
+ isLocked ? 'border-outline-variant bg-surface-container/50 hover:bg-surface-container opacity-70' :
337
+ isSelected
338
+ ? 'border-primary bg-primary/5 shadow-[0_0_0_1px_#2563eb]'
339
+ : 'border-outline-variant bg-surface-container-lowest hover:bg-surface-container-low'
340
+ }`}
341
+ >
342
+ <div className="flex justify-between items-start">
343
+ <div className={`h-10 w-10 rounded-full flex items-center justify-center transition-colors ${
344
+ isLocked ? 'bg-surface-container-high text-outline' :
345
+ isSelected
346
+ ? 'bg-primary/10 text-primary'
347
+ : 'bg-surface-container text-secondary group-hover:bg-surface-container-high'
348
+ }`}>
349
+ <span className="material-symbols-outlined">{isLocked ? (isQuotaExceeded ? 'workspace_premium' : 'lock') : method.icon}</span>
350
+ </div>
351
+
352
+ {!isLocked && (
353
+ <span className={`material-symbols-outlined transition-all ${isSelected
354
+ ? 'opacity-100 text-primary'
355
+ : 'opacity-0 text-outline'
356
+ }`} style={{ fontVariationSettings: isSelected ? "'FILL' 1" : "normal" }}>
357
+ check_circle
358
+ </span>
359
+ )}
360
+ {isLocked && (
361
+ <span className="text-[10px] font-bold uppercase tracking-wider px-2 py-1 bg-surface-container-high text-on-surface-variant rounded-md">
362
+ {isQuotaExceeded ? '0 Quota' : requiredTier}
363
+ </span>
364
+ )}
365
+ </div>
366
+
367
+ <div className="flex flex-col gap-xs mt-sm text-left">
368
+ <div className="flex items-center justify-between">
369
+ <span className="font-label-md text-label-md text-on-surface font-bold">
370
+ {method.title}
371
+ </span>
372
+ <span className="text-[11px] font-extrabold px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20">
373
+ {method.price}
374
+ </span>
375
+ </div>
376
+ <span className="font-body-sm text-body-sm text-on-surface-variant line-clamp-3">
377
+ {method.desc}
378
+ </span>
379
+ {method.tools && (
380
+ <div className="flex flex-wrap gap-xs mt-xs">
381
+ {method.tools.map(tool => (
382
+ <span key={tool} className={`text-[10px] font-bold uppercase tracking-wider px-[6px] py-[2px] rounded-full border ${
383
+ isLocked ? 'border-outline-variant text-outline bg-transparent' :
384
+ isSelected
385
+ ? 'border-primary/40 text-primary bg-primary/10'
386
+ : 'border-outline-variant text-on-surface-variant bg-surface-container'
387
+ }`}>
388
+ {tool}
389
+ </span>
390
+ ))}
391
+ </div>
392
+ )}
393
+ </div>
394
+
395
+ <div className="flex items-center justify-between mt-auto pt-sm">
396
+ <span className={`font-label-sm text-label-sm ${
397
+ isLocked ? 'text-outline font-bold' :
398
+ isSelected ? 'text-primary font-bold' : 'text-secondary'
399
+ }`}>
400
+ ⏱ {method.duration}
401
+ </span>
402
+ <span className="text-[11px] font-semibold text-on-surface-variant">
403
+ {method.id === 'Quick' ? '13 modules' : method.id === 'Advanced' ? '36 modules' : '89 modules'}
404
+ </span>
405
+ </div>
406
+ </div>
407
+ );
408
+ })}
409
+ </div>
410
+ </div>
411
+
412
+ <hr className="border-outline-variant border-t" />
413
+
414
+ {/* Schedule Scan Section */}
415
+ <div className="flex flex-col gap-sm">
416
+ <label className="flex items-center gap-sm cursor-pointer font-body-sm text-on-surface">
417
+ <input
418
+ type="checkbox"
419
+ checked={isScheduled}
420
+ onChange={(e) => setIsScheduled(e.target.checked)}
421
+ className="h-4 w-4 rounded border-outline-variant text-primary focus:ring-primary/30"
422
+ />
423
+ <span className="font-semibold uppercase tracking-wider font-label-sm">Schedule Automated Scans</span>
424
+ <span className="text-on-surface-variant text-[12px]">— Setup recurring scans (Enterprise feature)</span>
425
+ </label>
426
+
427
+ {isScheduled && (
428
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-md p-md bg-surface-container-low dark:bg-inverse-surface rounded-lg mt-xs border border-outline-variant/60">
429
+ <div className="flex flex-col gap-xs">
430
+ <label className="font-label-sm text-label-sm text-on-surface-variant uppercase tracking-wider font-semibold">Frequency</label>
431
+ <select
432
+ className="w-full bg-surface-container-lowest border border-outline-variant rounded px-md py-sm font-body-sm text-on-surface focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all cursor-pointer"
433
+ value={scheduleFrequency}
434
+ onChange={(e) => setScheduleFrequency(e.target.value)}
435
+ >
436
+ <option value="daily">Daily</option>
437
+ <option value="weekly">Weekly (Sundays)</option>
438
+ </select>
439
+ </div>
440
+ <div className="flex flex-col gap-xs">
441
+ <label className="font-label-sm text-label-sm text-on-surface-variant uppercase tracking-wider font-semibold">Time (UTC)</label>
442
+ <input
443
+ type="time"
444
+ className="w-full bg-surface-container-lowest border border-outline-variant rounded px-md py-sm font-body-sm text-on-surface focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all"
445
+ value={scheduleTime}
446
+ onChange={(e) => setScheduleTime(e.target.value)}
447
+ />
448
+ </div>
449
+ </div>
450
+ )}
451
+ </div>
452
+
453
+ <hr className="border-outline-variant border-t" />
454
+
455
+ {/* Legal Warning Notice */}
456
+ <div className="bg-surface-container-low dark:bg-inverse-surface border-l-4 border-primary p-md rounded-lg text-body-sm font-body-sm text-on-surface-variant leading-relaxed">
457
+ <strong className="text-on-surface font-semibold uppercase tracking-wider block mb-[4px]">
458
+ Operator Notice & Policy Compliance
459
+ </strong>
460
+ Conducting vulnerability analysis scans against networks or hosts without explicit, verified written authorization is illegal. By executing this scan, you certify that you possess the necessary regulatory clearance to target this host.
461
+ </div>
462
+
463
+ {/* Action Area */}
464
+ <div className="flex justify-end pt-sm border-t border-outline-variant/50">
465
+ <button
466
+ type="submit"
467
+ disabled={loading}
468
+ className="bg-primary text-on-primary font-label-md text-label-md px-xl py-md rounded-lg flex items-center gap-sm hover:opacity-90 transition-opacity shadow-sm font-bold border-0 cursor-pointer"
469
+ >
470
+ {loading ? (
471
+ <>
472
+ <span className="material-symbols-outlined animate-spin text-[18px]">sync</span>
473
+ {isScheduled ? "Scheduling..." : "Executing Pipeline..."}
474
+ </>
475
+ ) : (
476
+ <>
477
+ <span className="material-symbols-outlined text-[18px]" style={{ fontVariationSettings: "'FILL' 1" }}>
478
+ {isScheduled ? "calendar_month" : "play_arrow"}
479
+ </span>
480
+ {isScheduled ? "Schedule Automation" : "Execute Scan Pipeline"}
481
+ </>
482
+ )}
483
+ </button>
484
+ </div>
485
+ </form>
486
+
487
+ {/* Subscription Tier Required Modal */}
488
+ {showUpgradeModal && (
489
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-md">
490
+ <div className="absolute inset-0 bg-scrim/50 backdrop-blur-sm" onClick={() => setShowUpgradeModal(false)}></div>
491
+ <div className="relative bg-surface-container border border-outline-variant rounded-xl shadow-lg max-w-md w-full flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-200">
492
+ <div className="bg-surface-container-highest p-md border-b border-outline-variant flex justify-between items-center">
493
+ <h3 className="font-headline-sm text-headline-sm text-on-surface flex items-center gap-sm">
494
+ <span className="material-symbols-outlined text-primary">lock</span>
495
+ Subscription Required
496
+ </h3>
497
+ <button onClick={() => setShowUpgradeModal(false)} className="text-on-surface-variant hover:text-on-surface transition-colors cursor-pointer bg-transparent border-0">
498
+ <span className="material-symbols-outlined">close</span>
499
+ </button>
500
+ </div>
501
+ <div className="p-xl flex flex-col gap-md text-left">
502
+ <p className="font-body-md text-body-md text-on-surface-variant">
503
+ You cannot use the <strong className="text-on-surface">{attemptedScan}</strong> methodology on your current plan.
504
+ </p>
505
+ <p className="font-body-md text-body-md text-on-surface-variant">
506
+ If you need to use this feature, please upgrade your subscription plan to unlock advanced vulnerability scanning capabilities.
507
+ </p>
508
+ </div>
509
+ <div className="p-md bg-surface-container-low border-t border-outline-variant flex justify-end gap-sm">
510
+ <button
511
+ onClick={() => setShowUpgradeModal(false)}
512
+ className="px-md py-sm rounded-lg font-label-md text-label-md text-on-surface hover:bg-surface-container-highest transition-colors cursor-pointer border border-outline-variant bg-transparent"
513
+ >
514
+ Cancel
515
+ </button>
516
+ <button
517
+ onClick={() => {
518
+ setShowUpgradeModal(false);
519
+ navigate('/pricing');
520
+ }}
521
+ className="px-md py-sm rounded-lg font-label-md text-label-md bg-primary text-on-primary hover:opacity-90 transition-opacity cursor-pointer border-0 shadow-sm flex items-center gap-xs"
522
+ >
523
+ View Plans
524
+ <span className="material-symbols-outlined text-[18px]">arrow_forward</span>
525
+ </button>
526
+ </div>
527
+ </div>
528
+ </div>
529
+ )}
530
+
531
+ {/* Scan Quota Exceeded Modal Popup */}
532
+ {showQuotaExceededModal && (
533
+ <div className="fixed inset-0 z-[9999] flex items-center justify-center p-4">
534
+ {/* Backdrop */}
535
+ <div
536
+ className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity animate-fade-in"
537
+ onClick={() => setShowQuotaExceededModal(false)}
538
+ ></div>
539
+
540
+ {/* Modal Content */}
541
+ <div className="relative bg-white rounded-3xl border border-slate-200 shadow-2xl max-w-md w-full p-8 overflow-hidden z-10 animate-slide-up text-left">
542
+
543
+ {/* Top Glowing Icon Circle */}
544
+ <div className="w-14 h-14 rounded-2xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-center mb-5">
545
+ <span className="material-symbols-outlined text-amber-600 text-[32px]">workspace_premium</span>
546
+ </div>
547
+
548
+ {/* Header Badge */}
549
+ <div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-amber-50 border border-amber-200 text-amber-700 text-xs font-bold mb-3">
550
+ <span className="w-2 h-2 rounded-full bg-amber-500 animate-pulse"></span>
551
+ <span>Quota Limit Reached (0 Scans Left)</span>
552
+ </div>
553
+
554
+ {/* Modal Title */}
555
+ <h3 className="text-2xl font-bold text-slate-900 tracking-tight mb-2">
556
+ Scanning Quota Exhausted
557
+ </h3>
558
+
559
+ {/* Modal Description */}
560
+ <p className="text-sm text-slate-600 leading-relaxed mb-6">
561
+ Your organization has used all allocated scan credits for <strong className="text-slate-900">{attemptedScan || scanType} Scans</strong>. To execute additional vulnerability scans, please upgrade your plan or purchase scan credits.
562
+ </p>
563
+
564
+ {/* Action Buttons */}
565
+ <div className="flex items-center justify-end gap-3 pt-4 border-t border-slate-100">
566
+ <button
567
+ type="button"
568
+ onClick={() => setShowQuotaExceededModal(false)}
569
+ className="px-5 py-2.5 rounded-xl text-sm font-semibold text-slate-600 hover:bg-slate-100 transition-colors border border-slate-200 cursor-pointer bg-white"
570
+ >
571
+ Cancel
572
+ </button>
573
+ <button
574
+ type="button"
575
+ onClick={() => {
576
+ setShowQuotaExceededModal(false);
577
+ navigate('/pricing');
578
+ }}
579
+ className="px-5 py-2.5 rounded-xl text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 active:scale-95 transition-all shadow-md shadow-blue-600/20 flex items-center gap-2 cursor-pointer border-0"
580
+ >
581
+ <span>Upgrade Plan</span>
582
+ <span className="material-symbols-outlined text-[18px]">arrow_forward</span>
583
+ </button>
584
+ </div>
585
+ </div>
586
+ </div>
587
+ )}
588
+
589
+ </div>
590
+ );
591
+ };