Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Update frontend/src/pages/NewScan.jsx
Browse files- 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 [
|
| 19 |
-
const [
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
const [
|
| 23 |
-
const [
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
const
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
.then(
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
.then(
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
if (
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
const
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
<
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
)
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
<
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
<
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
</
|
| 376 |
-
<span className="
|
| 377 |
-
{method.
|
| 378 |
-
</span>
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
</
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
</>
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
<
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
<div className="
|
| 465 |
-
<
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
<
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
</
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
<
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 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 |
+
};
|