Spaces:
Sleeping
Sleeping
Commit ·
808332c
1
Parent(s): 5dc261b
UPDATE: UI and client assets
Browse files- .gitignore +3 -0
- client/index.html +18 -0
- client/package-lock.json +1794 -0
- client/package.json +19 -0
- client/src/App.jsx +78 -0
- client/src/api.js +35 -0
- client/src/components/RiskViz.jsx +184 -0
- client/src/components/TokenBar.jsx +20 -0
- client/src/components/Topbar.jsx +21 -0
- client/src/components/ui.jsx +72 -0
- client/src/index.css +707 -0
- client/src/main.jsx +10 -0
- client/src/tabs/AdminTab.jsx +204 -0
- client/src/tabs/ApiTab.jsx +247 -0
- client/src/tabs/IntelTab.jsx +629 -0
- client/src/tabs/Scenario1Tab.jsx +214 -0
- client/src/tabs/Scenario2Tab.jsx +346 -0
- client/vite.config.js +21 -0
- static/assets/index-C4kGMRC-.js +0 -0
- static/assets/index-DKV7YaMs.css +1 -0
- static/index.html +19 -1570
.gitignore
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Python build artifacts
|
| 2 |
__pycache__/
|
| 3 |
*.pyc
|
|
|
|
| 1 |
+
# Node
|
| 2 |
+
node_modules/
|
| 3 |
+
|
| 4 |
# Python build artifacts
|
| 5 |
__pycache__/
|
| 6 |
*.pyc
|
client/index.html
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>AdaptiveAuth — Framework Demo</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 9 |
+
<link
|
| 10 |
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
|
| 11 |
+
rel="stylesheet"
|
| 12 |
+
/>
|
| 13 |
+
</head>
|
| 14 |
+
<body>
|
| 15 |
+
<div id="root"></div>
|
| 16 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 17 |
+
</body>
|
| 18 |
+
</html>
|
client/package-lock.json
ADDED
|
@@ -0,0 +1,1794 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "adaptiveauth-ui",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "adaptiveauth-ui",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"react": "^18.3.1",
|
| 12 |
+
"react-dom": "^18.3.1"
|
| 13 |
+
},
|
| 14 |
+
"devDependencies": {
|
| 15 |
+
"@vitejs/plugin-react": "^4.3.4",
|
| 16 |
+
"vite": "^6.0.0"
|
| 17 |
+
}
|
| 18 |
+
},
|
| 19 |
+
"node_modules/@babel/code-frame": {
|
| 20 |
+
"version": "7.29.0",
|
| 21 |
+
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
| 22 |
+
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
| 23 |
+
"dev": true,
|
| 24 |
+
"license": "MIT",
|
| 25 |
+
"dependencies": {
|
| 26 |
+
"@babel/helper-validator-identifier": "^7.28.5",
|
| 27 |
+
"js-tokens": "^4.0.0",
|
| 28 |
+
"picocolors": "^1.1.1"
|
| 29 |
+
},
|
| 30 |
+
"engines": {
|
| 31 |
+
"node": ">=6.9.0"
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
"node_modules/@babel/compat-data": {
|
| 35 |
+
"version": "7.29.0",
|
| 36 |
+
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
|
| 37 |
+
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
|
| 38 |
+
"dev": true,
|
| 39 |
+
"license": "MIT",
|
| 40 |
+
"engines": {
|
| 41 |
+
"node": ">=6.9.0"
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
"node_modules/@babel/core": {
|
| 45 |
+
"version": "7.29.0",
|
| 46 |
+
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
| 47 |
+
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
| 48 |
+
"dev": true,
|
| 49 |
+
"license": "MIT",
|
| 50 |
+
"dependencies": {
|
| 51 |
+
"@babel/code-frame": "^7.29.0",
|
| 52 |
+
"@babel/generator": "^7.29.0",
|
| 53 |
+
"@babel/helper-compilation-targets": "^7.28.6",
|
| 54 |
+
"@babel/helper-module-transforms": "^7.28.6",
|
| 55 |
+
"@babel/helpers": "^7.28.6",
|
| 56 |
+
"@babel/parser": "^7.29.0",
|
| 57 |
+
"@babel/template": "^7.28.6",
|
| 58 |
+
"@babel/traverse": "^7.29.0",
|
| 59 |
+
"@babel/types": "^7.29.0",
|
| 60 |
+
"@jridgewell/remapping": "^2.3.5",
|
| 61 |
+
"convert-source-map": "^2.0.0",
|
| 62 |
+
"debug": "^4.1.0",
|
| 63 |
+
"gensync": "^1.0.0-beta.2",
|
| 64 |
+
"json5": "^2.2.3",
|
| 65 |
+
"semver": "^6.3.1"
|
| 66 |
+
},
|
| 67 |
+
"engines": {
|
| 68 |
+
"node": ">=6.9.0"
|
| 69 |
+
},
|
| 70 |
+
"funding": {
|
| 71 |
+
"type": "opencollective",
|
| 72 |
+
"url": "https://opencollective.com/babel"
|
| 73 |
+
}
|
| 74 |
+
},
|
| 75 |
+
"node_modules/@babel/generator": {
|
| 76 |
+
"version": "7.29.1",
|
| 77 |
+
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
|
| 78 |
+
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
| 79 |
+
"dev": true,
|
| 80 |
+
"license": "MIT",
|
| 81 |
+
"dependencies": {
|
| 82 |
+
"@babel/parser": "^7.29.0",
|
| 83 |
+
"@babel/types": "^7.29.0",
|
| 84 |
+
"@jridgewell/gen-mapping": "^0.3.12",
|
| 85 |
+
"@jridgewell/trace-mapping": "^0.3.28",
|
| 86 |
+
"jsesc": "^3.0.2"
|
| 87 |
+
},
|
| 88 |
+
"engines": {
|
| 89 |
+
"node": ">=6.9.0"
|
| 90 |
+
}
|
| 91 |
+
},
|
| 92 |
+
"node_modules/@babel/helper-compilation-targets": {
|
| 93 |
+
"version": "7.28.6",
|
| 94 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
|
| 95 |
+
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
|
| 96 |
+
"dev": true,
|
| 97 |
+
"license": "MIT",
|
| 98 |
+
"dependencies": {
|
| 99 |
+
"@babel/compat-data": "^7.28.6",
|
| 100 |
+
"@babel/helper-validator-option": "^7.27.1",
|
| 101 |
+
"browserslist": "^4.24.0",
|
| 102 |
+
"lru-cache": "^5.1.1",
|
| 103 |
+
"semver": "^6.3.1"
|
| 104 |
+
},
|
| 105 |
+
"engines": {
|
| 106 |
+
"node": ">=6.9.0"
|
| 107 |
+
}
|
| 108 |
+
},
|
| 109 |
+
"node_modules/@babel/helper-globals": {
|
| 110 |
+
"version": "7.28.0",
|
| 111 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
| 112 |
+
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
| 113 |
+
"dev": true,
|
| 114 |
+
"license": "MIT",
|
| 115 |
+
"engines": {
|
| 116 |
+
"node": ">=6.9.0"
|
| 117 |
+
}
|
| 118 |
+
},
|
| 119 |
+
"node_modules/@babel/helper-module-imports": {
|
| 120 |
+
"version": "7.28.6",
|
| 121 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
|
| 122 |
+
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
|
| 123 |
+
"dev": true,
|
| 124 |
+
"license": "MIT",
|
| 125 |
+
"dependencies": {
|
| 126 |
+
"@babel/traverse": "^7.28.6",
|
| 127 |
+
"@babel/types": "^7.28.6"
|
| 128 |
+
},
|
| 129 |
+
"engines": {
|
| 130 |
+
"node": ">=6.9.0"
|
| 131 |
+
}
|
| 132 |
+
},
|
| 133 |
+
"node_modules/@babel/helper-module-transforms": {
|
| 134 |
+
"version": "7.28.6",
|
| 135 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
|
| 136 |
+
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
|
| 137 |
+
"dev": true,
|
| 138 |
+
"license": "MIT",
|
| 139 |
+
"dependencies": {
|
| 140 |
+
"@babel/helper-module-imports": "^7.28.6",
|
| 141 |
+
"@babel/helper-validator-identifier": "^7.28.5",
|
| 142 |
+
"@babel/traverse": "^7.28.6"
|
| 143 |
+
},
|
| 144 |
+
"engines": {
|
| 145 |
+
"node": ">=6.9.0"
|
| 146 |
+
},
|
| 147 |
+
"peerDependencies": {
|
| 148 |
+
"@babel/core": "^7.0.0"
|
| 149 |
+
}
|
| 150 |
+
},
|
| 151 |
+
"node_modules/@babel/helper-plugin-utils": {
|
| 152 |
+
"version": "7.28.6",
|
| 153 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
|
| 154 |
+
"integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
|
| 155 |
+
"dev": true,
|
| 156 |
+
"license": "MIT",
|
| 157 |
+
"engines": {
|
| 158 |
+
"node": ">=6.9.0"
|
| 159 |
+
}
|
| 160 |
+
},
|
| 161 |
+
"node_modules/@babel/helper-string-parser": {
|
| 162 |
+
"version": "7.27.1",
|
| 163 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
| 164 |
+
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
| 165 |
+
"dev": true,
|
| 166 |
+
"license": "MIT",
|
| 167 |
+
"engines": {
|
| 168 |
+
"node": ">=6.9.0"
|
| 169 |
+
}
|
| 170 |
+
},
|
| 171 |
+
"node_modules/@babel/helper-validator-identifier": {
|
| 172 |
+
"version": "7.28.5",
|
| 173 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
| 174 |
+
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
| 175 |
+
"dev": true,
|
| 176 |
+
"license": "MIT",
|
| 177 |
+
"engines": {
|
| 178 |
+
"node": ">=6.9.0"
|
| 179 |
+
}
|
| 180 |
+
},
|
| 181 |
+
"node_modules/@babel/helper-validator-option": {
|
| 182 |
+
"version": "7.27.1",
|
| 183 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
| 184 |
+
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
| 185 |
+
"dev": true,
|
| 186 |
+
"license": "MIT",
|
| 187 |
+
"engines": {
|
| 188 |
+
"node": ">=6.9.0"
|
| 189 |
+
}
|
| 190 |
+
},
|
| 191 |
+
"node_modules/@babel/helpers": {
|
| 192 |
+
"version": "7.28.6",
|
| 193 |
+
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
|
| 194 |
+
"integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
|
| 195 |
+
"dev": true,
|
| 196 |
+
"license": "MIT",
|
| 197 |
+
"dependencies": {
|
| 198 |
+
"@babel/template": "^7.28.6",
|
| 199 |
+
"@babel/types": "^7.28.6"
|
| 200 |
+
},
|
| 201 |
+
"engines": {
|
| 202 |
+
"node": ">=6.9.0"
|
| 203 |
+
}
|
| 204 |
+
},
|
| 205 |
+
"node_modules/@babel/parser": {
|
| 206 |
+
"version": "7.29.0",
|
| 207 |
+
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
|
| 208 |
+
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
|
| 209 |
+
"dev": true,
|
| 210 |
+
"license": "MIT",
|
| 211 |
+
"dependencies": {
|
| 212 |
+
"@babel/types": "^7.29.0"
|
| 213 |
+
},
|
| 214 |
+
"bin": {
|
| 215 |
+
"parser": "bin/babel-parser.js"
|
| 216 |
+
},
|
| 217 |
+
"engines": {
|
| 218 |
+
"node": ">=6.0.0"
|
| 219 |
+
}
|
| 220 |
+
},
|
| 221 |
+
"node_modules/@babel/plugin-transform-react-jsx-self": {
|
| 222 |
+
"version": "7.27.1",
|
| 223 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
|
| 224 |
+
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
|
| 225 |
+
"dev": true,
|
| 226 |
+
"license": "MIT",
|
| 227 |
+
"dependencies": {
|
| 228 |
+
"@babel/helper-plugin-utils": "^7.27.1"
|
| 229 |
+
},
|
| 230 |
+
"engines": {
|
| 231 |
+
"node": ">=6.9.0"
|
| 232 |
+
},
|
| 233 |
+
"peerDependencies": {
|
| 234 |
+
"@babel/core": "^7.0.0-0"
|
| 235 |
+
}
|
| 236 |
+
},
|
| 237 |
+
"node_modules/@babel/plugin-transform-react-jsx-source": {
|
| 238 |
+
"version": "7.27.1",
|
| 239 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
|
| 240 |
+
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
|
| 241 |
+
"dev": true,
|
| 242 |
+
"license": "MIT",
|
| 243 |
+
"dependencies": {
|
| 244 |
+
"@babel/helper-plugin-utils": "^7.27.1"
|
| 245 |
+
},
|
| 246 |
+
"engines": {
|
| 247 |
+
"node": ">=6.9.0"
|
| 248 |
+
},
|
| 249 |
+
"peerDependencies": {
|
| 250 |
+
"@babel/core": "^7.0.0-0"
|
| 251 |
+
}
|
| 252 |
+
},
|
| 253 |
+
"node_modules/@babel/template": {
|
| 254 |
+
"version": "7.28.6",
|
| 255 |
+
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
| 256 |
+
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
| 257 |
+
"dev": true,
|
| 258 |
+
"license": "MIT",
|
| 259 |
+
"dependencies": {
|
| 260 |
+
"@babel/code-frame": "^7.28.6",
|
| 261 |
+
"@babel/parser": "^7.28.6",
|
| 262 |
+
"@babel/types": "^7.28.6"
|
| 263 |
+
},
|
| 264 |
+
"engines": {
|
| 265 |
+
"node": ">=6.9.0"
|
| 266 |
+
}
|
| 267 |
+
},
|
| 268 |
+
"node_modules/@babel/traverse": {
|
| 269 |
+
"version": "7.29.0",
|
| 270 |
+
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
|
| 271 |
+
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
| 272 |
+
"dev": true,
|
| 273 |
+
"license": "MIT",
|
| 274 |
+
"dependencies": {
|
| 275 |
+
"@babel/code-frame": "^7.29.0",
|
| 276 |
+
"@babel/generator": "^7.29.0",
|
| 277 |
+
"@babel/helper-globals": "^7.28.0",
|
| 278 |
+
"@babel/parser": "^7.29.0",
|
| 279 |
+
"@babel/template": "^7.28.6",
|
| 280 |
+
"@babel/types": "^7.29.0",
|
| 281 |
+
"debug": "^4.3.1"
|
| 282 |
+
},
|
| 283 |
+
"engines": {
|
| 284 |
+
"node": ">=6.9.0"
|
| 285 |
+
}
|
| 286 |
+
},
|
| 287 |
+
"node_modules/@babel/types": {
|
| 288 |
+
"version": "7.29.0",
|
| 289 |
+
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
| 290 |
+
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
| 291 |
+
"dev": true,
|
| 292 |
+
"license": "MIT",
|
| 293 |
+
"dependencies": {
|
| 294 |
+
"@babel/helper-string-parser": "^7.27.1",
|
| 295 |
+
"@babel/helper-validator-identifier": "^7.28.5"
|
| 296 |
+
},
|
| 297 |
+
"engines": {
|
| 298 |
+
"node": ">=6.9.0"
|
| 299 |
+
}
|
| 300 |
+
},
|
| 301 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 302 |
+
"version": "0.25.12",
|
| 303 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
| 304 |
+
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
| 305 |
+
"cpu": [
|
| 306 |
+
"ppc64"
|
| 307 |
+
],
|
| 308 |
+
"dev": true,
|
| 309 |
+
"license": "MIT",
|
| 310 |
+
"optional": true,
|
| 311 |
+
"os": [
|
| 312 |
+
"aix"
|
| 313 |
+
],
|
| 314 |
+
"engines": {
|
| 315 |
+
"node": ">=18"
|
| 316 |
+
}
|
| 317 |
+
},
|
| 318 |
+
"node_modules/@esbuild/android-arm": {
|
| 319 |
+
"version": "0.25.12",
|
| 320 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
| 321 |
+
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
| 322 |
+
"cpu": [
|
| 323 |
+
"arm"
|
| 324 |
+
],
|
| 325 |
+
"dev": true,
|
| 326 |
+
"license": "MIT",
|
| 327 |
+
"optional": true,
|
| 328 |
+
"os": [
|
| 329 |
+
"android"
|
| 330 |
+
],
|
| 331 |
+
"engines": {
|
| 332 |
+
"node": ">=18"
|
| 333 |
+
}
|
| 334 |
+
},
|
| 335 |
+
"node_modules/@esbuild/android-arm64": {
|
| 336 |
+
"version": "0.25.12",
|
| 337 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
| 338 |
+
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
| 339 |
+
"cpu": [
|
| 340 |
+
"arm64"
|
| 341 |
+
],
|
| 342 |
+
"dev": true,
|
| 343 |
+
"license": "MIT",
|
| 344 |
+
"optional": true,
|
| 345 |
+
"os": [
|
| 346 |
+
"android"
|
| 347 |
+
],
|
| 348 |
+
"engines": {
|
| 349 |
+
"node": ">=18"
|
| 350 |
+
}
|
| 351 |
+
},
|
| 352 |
+
"node_modules/@esbuild/android-x64": {
|
| 353 |
+
"version": "0.25.12",
|
| 354 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
| 355 |
+
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
| 356 |
+
"cpu": [
|
| 357 |
+
"x64"
|
| 358 |
+
],
|
| 359 |
+
"dev": true,
|
| 360 |
+
"license": "MIT",
|
| 361 |
+
"optional": true,
|
| 362 |
+
"os": [
|
| 363 |
+
"android"
|
| 364 |
+
],
|
| 365 |
+
"engines": {
|
| 366 |
+
"node": ">=18"
|
| 367 |
+
}
|
| 368 |
+
},
|
| 369 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 370 |
+
"version": "0.25.12",
|
| 371 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
| 372 |
+
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
| 373 |
+
"cpu": [
|
| 374 |
+
"arm64"
|
| 375 |
+
],
|
| 376 |
+
"dev": true,
|
| 377 |
+
"license": "MIT",
|
| 378 |
+
"optional": true,
|
| 379 |
+
"os": [
|
| 380 |
+
"darwin"
|
| 381 |
+
],
|
| 382 |
+
"engines": {
|
| 383 |
+
"node": ">=18"
|
| 384 |
+
}
|
| 385 |
+
},
|
| 386 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 387 |
+
"version": "0.25.12",
|
| 388 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
| 389 |
+
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
| 390 |
+
"cpu": [
|
| 391 |
+
"x64"
|
| 392 |
+
],
|
| 393 |
+
"dev": true,
|
| 394 |
+
"license": "MIT",
|
| 395 |
+
"optional": true,
|
| 396 |
+
"os": [
|
| 397 |
+
"darwin"
|
| 398 |
+
],
|
| 399 |
+
"engines": {
|
| 400 |
+
"node": ">=18"
|
| 401 |
+
}
|
| 402 |
+
},
|
| 403 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 404 |
+
"version": "0.25.12",
|
| 405 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
| 406 |
+
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
| 407 |
+
"cpu": [
|
| 408 |
+
"arm64"
|
| 409 |
+
],
|
| 410 |
+
"dev": true,
|
| 411 |
+
"license": "MIT",
|
| 412 |
+
"optional": true,
|
| 413 |
+
"os": [
|
| 414 |
+
"freebsd"
|
| 415 |
+
],
|
| 416 |
+
"engines": {
|
| 417 |
+
"node": ">=18"
|
| 418 |
+
}
|
| 419 |
+
},
|
| 420 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 421 |
+
"version": "0.25.12",
|
| 422 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
| 423 |
+
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
| 424 |
+
"cpu": [
|
| 425 |
+
"x64"
|
| 426 |
+
],
|
| 427 |
+
"dev": true,
|
| 428 |
+
"license": "MIT",
|
| 429 |
+
"optional": true,
|
| 430 |
+
"os": [
|
| 431 |
+
"freebsd"
|
| 432 |
+
],
|
| 433 |
+
"engines": {
|
| 434 |
+
"node": ">=18"
|
| 435 |
+
}
|
| 436 |
+
},
|
| 437 |
+
"node_modules/@esbuild/linux-arm": {
|
| 438 |
+
"version": "0.25.12",
|
| 439 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
| 440 |
+
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
| 441 |
+
"cpu": [
|
| 442 |
+
"arm"
|
| 443 |
+
],
|
| 444 |
+
"dev": true,
|
| 445 |
+
"license": "MIT",
|
| 446 |
+
"optional": true,
|
| 447 |
+
"os": [
|
| 448 |
+
"linux"
|
| 449 |
+
],
|
| 450 |
+
"engines": {
|
| 451 |
+
"node": ">=18"
|
| 452 |
+
}
|
| 453 |
+
},
|
| 454 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 455 |
+
"version": "0.25.12",
|
| 456 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
| 457 |
+
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
| 458 |
+
"cpu": [
|
| 459 |
+
"arm64"
|
| 460 |
+
],
|
| 461 |
+
"dev": true,
|
| 462 |
+
"license": "MIT",
|
| 463 |
+
"optional": true,
|
| 464 |
+
"os": [
|
| 465 |
+
"linux"
|
| 466 |
+
],
|
| 467 |
+
"engines": {
|
| 468 |
+
"node": ">=18"
|
| 469 |
+
}
|
| 470 |
+
},
|
| 471 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 472 |
+
"version": "0.25.12",
|
| 473 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
| 474 |
+
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
| 475 |
+
"cpu": [
|
| 476 |
+
"ia32"
|
| 477 |
+
],
|
| 478 |
+
"dev": true,
|
| 479 |
+
"license": "MIT",
|
| 480 |
+
"optional": true,
|
| 481 |
+
"os": [
|
| 482 |
+
"linux"
|
| 483 |
+
],
|
| 484 |
+
"engines": {
|
| 485 |
+
"node": ">=18"
|
| 486 |
+
}
|
| 487 |
+
},
|
| 488 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 489 |
+
"version": "0.25.12",
|
| 490 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
| 491 |
+
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
| 492 |
+
"cpu": [
|
| 493 |
+
"loong64"
|
| 494 |
+
],
|
| 495 |
+
"dev": true,
|
| 496 |
+
"license": "MIT",
|
| 497 |
+
"optional": true,
|
| 498 |
+
"os": [
|
| 499 |
+
"linux"
|
| 500 |
+
],
|
| 501 |
+
"engines": {
|
| 502 |
+
"node": ">=18"
|
| 503 |
+
}
|
| 504 |
+
},
|
| 505 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 506 |
+
"version": "0.25.12",
|
| 507 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
| 508 |
+
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
| 509 |
+
"cpu": [
|
| 510 |
+
"mips64el"
|
| 511 |
+
],
|
| 512 |
+
"dev": true,
|
| 513 |
+
"license": "MIT",
|
| 514 |
+
"optional": true,
|
| 515 |
+
"os": [
|
| 516 |
+
"linux"
|
| 517 |
+
],
|
| 518 |
+
"engines": {
|
| 519 |
+
"node": ">=18"
|
| 520 |
+
}
|
| 521 |
+
},
|
| 522 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 523 |
+
"version": "0.25.12",
|
| 524 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
| 525 |
+
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
| 526 |
+
"cpu": [
|
| 527 |
+
"ppc64"
|
| 528 |
+
],
|
| 529 |
+
"dev": true,
|
| 530 |
+
"license": "MIT",
|
| 531 |
+
"optional": true,
|
| 532 |
+
"os": [
|
| 533 |
+
"linux"
|
| 534 |
+
],
|
| 535 |
+
"engines": {
|
| 536 |
+
"node": ">=18"
|
| 537 |
+
}
|
| 538 |
+
},
|
| 539 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 540 |
+
"version": "0.25.12",
|
| 541 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
| 542 |
+
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
| 543 |
+
"cpu": [
|
| 544 |
+
"riscv64"
|
| 545 |
+
],
|
| 546 |
+
"dev": true,
|
| 547 |
+
"license": "MIT",
|
| 548 |
+
"optional": true,
|
| 549 |
+
"os": [
|
| 550 |
+
"linux"
|
| 551 |
+
],
|
| 552 |
+
"engines": {
|
| 553 |
+
"node": ">=18"
|
| 554 |
+
}
|
| 555 |
+
},
|
| 556 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 557 |
+
"version": "0.25.12",
|
| 558 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
| 559 |
+
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
| 560 |
+
"cpu": [
|
| 561 |
+
"s390x"
|
| 562 |
+
],
|
| 563 |
+
"dev": true,
|
| 564 |
+
"license": "MIT",
|
| 565 |
+
"optional": true,
|
| 566 |
+
"os": [
|
| 567 |
+
"linux"
|
| 568 |
+
],
|
| 569 |
+
"engines": {
|
| 570 |
+
"node": ">=18"
|
| 571 |
+
}
|
| 572 |
+
},
|
| 573 |
+
"node_modules/@esbuild/linux-x64": {
|
| 574 |
+
"version": "0.25.12",
|
| 575 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
| 576 |
+
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
| 577 |
+
"cpu": [
|
| 578 |
+
"x64"
|
| 579 |
+
],
|
| 580 |
+
"dev": true,
|
| 581 |
+
"license": "MIT",
|
| 582 |
+
"optional": true,
|
| 583 |
+
"os": [
|
| 584 |
+
"linux"
|
| 585 |
+
],
|
| 586 |
+
"engines": {
|
| 587 |
+
"node": ">=18"
|
| 588 |
+
}
|
| 589 |
+
},
|
| 590 |
+
"node_modules/@esbuild/netbsd-arm64": {
|
| 591 |
+
"version": "0.25.12",
|
| 592 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
| 593 |
+
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
| 594 |
+
"cpu": [
|
| 595 |
+
"arm64"
|
| 596 |
+
],
|
| 597 |
+
"dev": true,
|
| 598 |
+
"license": "MIT",
|
| 599 |
+
"optional": true,
|
| 600 |
+
"os": [
|
| 601 |
+
"netbsd"
|
| 602 |
+
],
|
| 603 |
+
"engines": {
|
| 604 |
+
"node": ">=18"
|
| 605 |
+
}
|
| 606 |
+
},
|
| 607 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 608 |
+
"version": "0.25.12",
|
| 609 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
| 610 |
+
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
| 611 |
+
"cpu": [
|
| 612 |
+
"x64"
|
| 613 |
+
],
|
| 614 |
+
"dev": true,
|
| 615 |
+
"license": "MIT",
|
| 616 |
+
"optional": true,
|
| 617 |
+
"os": [
|
| 618 |
+
"netbsd"
|
| 619 |
+
],
|
| 620 |
+
"engines": {
|
| 621 |
+
"node": ">=18"
|
| 622 |
+
}
|
| 623 |
+
},
|
| 624 |
+
"node_modules/@esbuild/openbsd-arm64": {
|
| 625 |
+
"version": "0.25.12",
|
| 626 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
| 627 |
+
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
| 628 |
+
"cpu": [
|
| 629 |
+
"arm64"
|
| 630 |
+
],
|
| 631 |
+
"dev": true,
|
| 632 |
+
"license": "MIT",
|
| 633 |
+
"optional": true,
|
| 634 |
+
"os": [
|
| 635 |
+
"openbsd"
|
| 636 |
+
],
|
| 637 |
+
"engines": {
|
| 638 |
+
"node": ">=18"
|
| 639 |
+
}
|
| 640 |
+
},
|
| 641 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 642 |
+
"version": "0.25.12",
|
| 643 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
| 644 |
+
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
| 645 |
+
"cpu": [
|
| 646 |
+
"x64"
|
| 647 |
+
],
|
| 648 |
+
"dev": true,
|
| 649 |
+
"license": "MIT",
|
| 650 |
+
"optional": true,
|
| 651 |
+
"os": [
|
| 652 |
+
"openbsd"
|
| 653 |
+
],
|
| 654 |
+
"engines": {
|
| 655 |
+
"node": ">=18"
|
| 656 |
+
}
|
| 657 |
+
},
|
| 658 |
+
"node_modules/@esbuild/openharmony-arm64": {
|
| 659 |
+
"version": "0.25.12",
|
| 660 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
| 661 |
+
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
| 662 |
+
"cpu": [
|
| 663 |
+
"arm64"
|
| 664 |
+
],
|
| 665 |
+
"dev": true,
|
| 666 |
+
"license": "MIT",
|
| 667 |
+
"optional": true,
|
| 668 |
+
"os": [
|
| 669 |
+
"openharmony"
|
| 670 |
+
],
|
| 671 |
+
"engines": {
|
| 672 |
+
"node": ">=18"
|
| 673 |
+
}
|
| 674 |
+
},
|
| 675 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 676 |
+
"version": "0.25.12",
|
| 677 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
| 678 |
+
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
| 679 |
+
"cpu": [
|
| 680 |
+
"x64"
|
| 681 |
+
],
|
| 682 |
+
"dev": true,
|
| 683 |
+
"license": "MIT",
|
| 684 |
+
"optional": true,
|
| 685 |
+
"os": [
|
| 686 |
+
"sunos"
|
| 687 |
+
],
|
| 688 |
+
"engines": {
|
| 689 |
+
"node": ">=18"
|
| 690 |
+
}
|
| 691 |
+
},
|
| 692 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 693 |
+
"version": "0.25.12",
|
| 694 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
| 695 |
+
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
| 696 |
+
"cpu": [
|
| 697 |
+
"arm64"
|
| 698 |
+
],
|
| 699 |
+
"dev": true,
|
| 700 |
+
"license": "MIT",
|
| 701 |
+
"optional": true,
|
| 702 |
+
"os": [
|
| 703 |
+
"win32"
|
| 704 |
+
],
|
| 705 |
+
"engines": {
|
| 706 |
+
"node": ">=18"
|
| 707 |
+
}
|
| 708 |
+
},
|
| 709 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 710 |
+
"version": "0.25.12",
|
| 711 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
| 712 |
+
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
| 713 |
+
"cpu": [
|
| 714 |
+
"ia32"
|
| 715 |
+
],
|
| 716 |
+
"dev": true,
|
| 717 |
+
"license": "MIT",
|
| 718 |
+
"optional": true,
|
| 719 |
+
"os": [
|
| 720 |
+
"win32"
|
| 721 |
+
],
|
| 722 |
+
"engines": {
|
| 723 |
+
"node": ">=18"
|
| 724 |
+
}
|
| 725 |
+
},
|
| 726 |
+
"node_modules/@esbuild/win32-x64": {
|
| 727 |
+
"version": "0.25.12",
|
| 728 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
| 729 |
+
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
| 730 |
+
"cpu": [
|
| 731 |
+
"x64"
|
| 732 |
+
],
|
| 733 |
+
"dev": true,
|
| 734 |
+
"license": "MIT",
|
| 735 |
+
"optional": true,
|
| 736 |
+
"os": [
|
| 737 |
+
"win32"
|
| 738 |
+
],
|
| 739 |
+
"engines": {
|
| 740 |
+
"node": ">=18"
|
| 741 |
+
}
|
| 742 |
+
},
|
| 743 |
+
"node_modules/@jridgewell/gen-mapping": {
|
| 744 |
+
"version": "0.3.13",
|
| 745 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
| 746 |
+
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
| 747 |
+
"dev": true,
|
| 748 |
+
"license": "MIT",
|
| 749 |
+
"dependencies": {
|
| 750 |
+
"@jridgewell/sourcemap-codec": "^1.5.0",
|
| 751 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 752 |
+
}
|
| 753 |
+
},
|
| 754 |
+
"node_modules/@jridgewell/remapping": {
|
| 755 |
+
"version": "2.3.5",
|
| 756 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
| 757 |
+
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
| 758 |
+
"dev": true,
|
| 759 |
+
"license": "MIT",
|
| 760 |
+
"dependencies": {
|
| 761 |
+
"@jridgewell/gen-mapping": "^0.3.5",
|
| 762 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 763 |
+
}
|
| 764 |
+
},
|
| 765 |
+
"node_modules/@jridgewell/resolve-uri": {
|
| 766 |
+
"version": "3.1.2",
|
| 767 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
| 768 |
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
| 769 |
+
"dev": true,
|
| 770 |
+
"license": "MIT",
|
| 771 |
+
"engines": {
|
| 772 |
+
"node": ">=6.0.0"
|
| 773 |
+
}
|
| 774 |
+
},
|
| 775 |
+
"node_modules/@jridgewell/sourcemap-codec": {
|
| 776 |
+
"version": "1.5.5",
|
| 777 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
| 778 |
+
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
| 779 |
+
"dev": true,
|
| 780 |
+
"license": "MIT"
|
| 781 |
+
},
|
| 782 |
+
"node_modules/@jridgewell/trace-mapping": {
|
| 783 |
+
"version": "0.3.31",
|
| 784 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
| 785 |
+
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
| 786 |
+
"dev": true,
|
| 787 |
+
"license": "MIT",
|
| 788 |
+
"dependencies": {
|
| 789 |
+
"@jridgewell/resolve-uri": "^3.1.0",
|
| 790 |
+
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 791 |
+
}
|
| 792 |
+
},
|
| 793 |
+
"node_modules/@rolldown/pluginutils": {
|
| 794 |
+
"version": "1.0.0-beta.27",
|
| 795 |
+
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
| 796 |
+
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
|
| 797 |
+
"dev": true,
|
| 798 |
+
"license": "MIT"
|
| 799 |
+
},
|
| 800 |
+
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 801 |
+
"version": "4.58.0",
|
| 802 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.58.0.tgz",
|
| 803 |
+
"integrity": "sha512-mr0tmS/4FoVk1cnaeN244A/wjvGDNItZKR8hRhnmCzygyRXYtKF5jVDSIILR1U97CTzAYmbgIj/Dukg62ggG5w==",
|
| 804 |
+
"cpu": [
|
| 805 |
+
"arm"
|
| 806 |
+
],
|
| 807 |
+
"dev": true,
|
| 808 |
+
"license": "MIT",
|
| 809 |
+
"optional": true,
|
| 810 |
+
"os": [
|
| 811 |
+
"android"
|
| 812 |
+
]
|
| 813 |
+
},
|
| 814 |
+
"node_modules/@rollup/rollup-android-arm64": {
|
| 815 |
+
"version": "4.58.0",
|
| 816 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.58.0.tgz",
|
| 817 |
+
"integrity": "sha512-+s++dbp+/RTte62mQD9wLSbiMTV+xr/PeRJEc/sFZFSBRlHPNPVaf5FXlzAL77Mr8FtSfQqCN+I598M8U41ccQ==",
|
| 818 |
+
"cpu": [
|
| 819 |
+
"arm64"
|
| 820 |
+
],
|
| 821 |
+
"dev": true,
|
| 822 |
+
"license": "MIT",
|
| 823 |
+
"optional": true,
|
| 824 |
+
"os": [
|
| 825 |
+
"android"
|
| 826 |
+
]
|
| 827 |
+
},
|
| 828 |
+
"node_modules/@rollup/rollup-darwin-arm64": {
|
| 829 |
+
"version": "4.58.0",
|
| 830 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.58.0.tgz",
|
| 831 |
+
"integrity": "sha512-MFWBwTcYs0jZbINQBXHfSrpSQJq3IUOakcKPzfeSznONop14Pxuqa0Kg19GD0rNBMPQI2tFtu3UzapZpH0Uc1Q==",
|
| 832 |
+
"cpu": [
|
| 833 |
+
"arm64"
|
| 834 |
+
],
|
| 835 |
+
"dev": true,
|
| 836 |
+
"license": "MIT",
|
| 837 |
+
"optional": true,
|
| 838 |
+
"os": [
|
| 839 |
+
"darwin"
|
| 840 |
+
]
|
| 841 |
+
},
|
| 842 |
+
"node_modules/@rollup/rollup-darwin-x64": {
|
| 843 |
+
"version": "4.58.0",
|
| 844 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.58.0.tgz",
|
| 845 |
+
"integrity": "sha512-yiKJY7pj9c9JwzuKYLFaDZw5gma3fI9bkPEIyofvVfsPqjCWPglSHdpdwXpKGvDeYDms3Qal8qGMEHZ1M/4Udg==",
|
| 846 |
+
"cpu": [
|
| 847 |
+
"x64"
|
| 848 |
+
],
|
| 849 |
+
"dev": true,
|
| 850 |
+
"license": "MIT",
|
| 851 |
+
"optional": true,
|
| 852 |
+
"os": [
|
| 853 |
+
"darwin"
|
| 854 |
+
]
|
| 855 |
+
},
|
| 856 |
+
"node_modules/@rollup/rollup-freebsd-arm64": {
|
| 857 |
+
"version": "4.58.0",
|
| 858 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.58.0.tgz",
|
| 859 |
+
"integrity": "sha512-x97kCoBh5MOevpn/CNK9W1x8BEzO238541BGWBc315uOlN0AD/ifZ1msg+ZQB05Ux+VF6EcYqpiagfLJ8U3LvQ==",
|
| 860 |
+
"cpu": [
|
| 861 |
+
"arm64"
|
| 862 |
+
],
|
| 863 |
+
"dev": true,
|
| 864 |
+
"license": "MIT",
|
| 865 |
+
"optional": true,
|
| 866 |
+
"os": [
|
| 867 |
+
"freebsd"
|
| 868 |
+
]
|
| 869 |
+
},
|
| 870 |
+
"node_modules/@rollup/rollup-freebsd-x64": {
|
| 871 |
+
"version": "4.58.0",
|
| 872 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.58.0.tgz",
|
| 873 |
+
"integrity": "sha512-Aa8jPoZ6IQAG2eIrcXPpjRcMjROMFxCt1UYPZZtCxRV68WkuSigYtQ/7Zwrcr2IvtNJo7T2JfDXyMLxq5L4Jlg==",
|
| 874 |
+
"cpu": [
|
| 875 |
+
"x64"
|
| 876 |
+
],
|
| 877 |
+
"dev": true,
|
| 878 |
+
"license": "MIT",
|
| 879 |
+
"optional": true,
|
| 880 |
+
"os": [
|
| 881 |
+
"freebsd"
|
| 882 |
+
]
|
| 883 |
+
},
|
| 884 |
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
| 885 |
+
"version": "4.58.0",
|
| 886 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.58.0.tgz",
|
| 887 |
+
"integrity": "sha512-Ob8YgT5kD/lSIYW2Rcngs5kNB/44Q2RzBSPz9brf2WEtcGR7/f/E9HeHn1wYaAwKBni+bdXEwgHvUd0x12lQSA==",
|
| 888 |
+
"cpu": [
|
| 889 |
+
"arm"
|
| 890 |
+
],
|
| 891 |
+
"dev": true,
|
| 892 |
+
"license": "MIT",
|
| 893 |
+
"optional": true,
|
| 894 |
+
"os": [
|
| 895 |
+
"linux"
|
| 896 |
+
]
|
| 897 |
+
},
|
| 898 |
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
| 899 |
+
"version": "4.58.0",
|
| 900 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.58.0.tgz",
|
| 901 |
+
"integrity": "sha512-K+RI5oP1ceqoadvNt1FecL17Qtw/n9BgRSzxif3rTL2QlIu88ccvY+Y9nnHe/cmT5zbH9+bpiJuG1mGHRVwF4Q==",
|
| 902 |
+
"cpu": [
|
| 903 |
+
"arm"
|
| 904 |
+
],
|
| 905 |
+
"dev": true,
|
| 906 |
+
"license": "MIT",
|
| 907 |
+
"optional": true,
|
| 908 |
+
"os": [
|
| 909 |
+
"linux"
|
| 910 |
+
]
|
| 911 |
+
},
|
| 912 |
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
| 913 |
+
"version": "4.58.0",
|
| 914 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.58.0.tgz",
|
| 915 |
+
"integrity": "sha512-T+17JAsCKUjmbopcKepJjHWHXSjeW7O5PL7lEFaeQmiVyw4kkc5/lyYKzrv6ElWRX/MrEWfPiJWqbTvfIvjM1Q==",
|
| 916 |
+
"cpu": [
|
| 917 |
+
"arm64"
|
| 918 |
+
],
|
| 919 |
+
"dev": true,
|
| 920 |
+
"license": "MIT",
|
| 921 |
+
"optional": true,
|
| 922 |
+
"os": [
|
| 923 |
+
"linux"
|
| 924 |
+
]
|
| 925 |
+
},
|
| 926 |
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
| 927 |
+
"version": "4.58.0",
|
| 928 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.58.0.tgz",
|
| 929 |
+
"integrity": "sha512-cCePktb9+6R9itIJdeCFF9txPU7pQeEHB5AbHu/MKsfH/k70ZtOeq1k4YAtBv9Z7mmKI5/wOLYjQ+B9QdxR6LA==",
|
| 930 |
+
"cpu": [
|
| 931 |
+
"arm64"
|
| 932 |
+
],
|
| 933 |
+
"dev": true,
|
| 934 |
+
"license": "MIT",
|
| 935 |
+
"optional": true,
|
| 936 |
+
"os": [
|
| 937 |
+
"linux"
|
| 938 |
+
]
|
| 939 |
+
},
|
| 940 |
+
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
| 941 |
+
"version": "4.58.0",
|
| 942 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.58.0.tgz",
|
| 943 |
+
"integrity": "sha512-iekUaLkfliAsDl4/xSdoCJ1gnnIXvoNz85C8U8+ZxknM5pBStfZjeXgB8lXobDQvvPRCN8FPmmuTtH+z95HTmg==",
|
| 944 |
+
"cpu": [
|
| 945 |
+
"loong64"
|
| 946 |
+
],
|
| 947 |
+
"dev": true,
|
| 948 |
+
"license": "MIT",
|
| 949 |
+
"optional": true,
|
| 950 |
+
"os": [
|
| 951 |
+
"linux"
|
| 952 |
+
]
|
| 953 |
+
},
|
| 954 |
+
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
| 955 |
+
"version": "4.58.0",
|
| 956 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.58.0.tgz",
|
| 957 |
+
"integrity": "sha512-68ofRgJNl/jYJbxFjCKE7IwhbfxOl1muPN4KbIqAIe32lm22KmU7E8OPvyy68HTNkI2iV/c8y2kSPSm2mW/Q9Q==",
|
| 958 |
+
"cpu": [
|
| 959 |
+
"loong64"
|
| 960 |
+
],
|
| 961 |
+
"dev": true,
|
| 962 |
+
"license": "MIT",
|
| 963 |
+
"optional": true,
|
| 964 |
+
"os": [
|
| 965 |
+
"linux"
|
| 966 |
+
]
|
| 967 |
+
},
|
| 968 |
+
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
| 969 |
+
"version": "4.58.0",
|
| 970 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.58.0.tgz",
|
| 971 |
+
"integrity": "sha512-dpz8vT0i+JqUKuSNPCP5SYyIV2Lh0sNL1+FhM7eLC457d5B9/BC3kDPp5BBftMmTNsBarcPcoz5UGSsnCiw4XQ==",
|
| 972 |
+
"cpu": [
|
| 973 |
+
"ppc64"
|
| 974 |
+
],
|
| 975 |
+
"dev": true,
|
| 976 |
+
"license": "MIT",
|
| 977 |
+
"optional": true,
|
| 978 |
+
"os": [
|
| 979 |
+
"linux"
|
| 980 |
+
]
|
| 981 |
+
},
|
| 982 |
+
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
| 983 |
+
"version": "4.58.0",
|
| 984 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.58.0.tgz",
|
| 985 |
+
"integrity": "sha512-4gdkkf9UJ7tafnweBCR/mk4jf3Jfl0cKX9Np80t5i78kjIH0ZdezUv/JDI2VtruE5lunfACqftJ8dIMGN4oHew==",
|
| 986 |
+
"cpu": [
|
| 987 |
+
"ppc64"
|
| 988 |
+
],
|
| 989 |
+
"dev": true,
|
| 990 |
+
"license": "MIT",
|
| 991 |
+
"optional": true,
|
| 992 |
+
"os": [
|
| 993 |
+
"linux"
|
| 994 |
+
]
|
| 995 |
+
},
|
| 996 |
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
| 997 |
+
"version": "4.58.0",
|
| 998 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.58.0.tgz",
|
| 999 |
+
"integrity": "sha512-YFS4vPnOkDTD/JriUeeZurFYoJhPf9GQQEF/v4lltp3mVcBmnsAdjEWhr2cjUCZzZNzxCG0HZOvJU44UGHSdzw==",
|
| 1000 |
+
"cpu": [
|
| 1001 |
+
"riscv64"
|
| 1002 |
+
],
|
| 1003 |
+
"dev": true,
|
| 1004 |
+
"license": "MIT",
|
| 1005 |
+
"optional": true,
|
| 1006 |
+
"os": [
|
| 1007 |
+
"linux"
|
| 1008 |
+
]
|
| 1009 |
+
},
|
| 1010 |
+
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
| 1011 |
+
"version": "4.58.0",
|
| 1012 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.58.0.tgz",
|
| 1013 |
+
"integrity": "sha512-x2xgZlFne+QVNKV8b4wwaCS8pwq3y14zedZ5DqLzjdRITvreBk//4Knbcvm7+lWmms9V9qFp60MtUd0/t/PXPw==",
|
| 1014 |
+
"cpu": [
|
| 1015 |
+
"riscv64"
|
| 1016 |
+
],
|
| 1017 |
+
"dev": true,
|
| 1018 |
+
"license": "MIT",
|
| 1019 |
+
"optional": true,
|
| 1020 |
+
"os": [
|
| 1021 |
+
"linux"
|
| 1022 |
+
]
|
| 1023 |
+
},
|
| 1024 |
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
| 1025 |
+
"version": "4.58.0",
|
| 1026 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.58.0.tgz",
|
| 1027 |
+
"integrity": "sha512-jIhrujyn4UnWF8S+DHSkAkDEO3hLX0cjzxJZPLF80xFyzyUIYgSMRcYQ3+uqEoyDD2beGq7Dj7edi8OnJcS/hg==",
|
| 1028 |
+
"cpu": [
|
| 1029 |
+
"s390x"
|
| 1030 |
+
],
|
| 1031 |
+
"dev": true,
|
| 1032 |
+
"license": "MIT",
|
| 1033 |
+
"optional": true,
|
| 1034 |
+
"os": [
|
| 1035 |
+
"linux"
|
| 1036 |
+
]
|
| 1037 |
+
},
|
| 1038 |
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
| 1039 |
+
"version": "4.58.0",
|
| 1040 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.58.0.tgz",
|
| 1041 |
+
"integrity": "sha512-+410Srdoh78MKSJxTQ+hZ/Mx+ajd6RjjPwBPNd0R3J9FtL6ZA0GqiiyNjCO9In0IzZkCNrpGymSfn+kgyPQocg==",
|
| 1042 |
+
"cpu": [
|
| 1043 |
+
"x64"
|
| 1044 |
+
],
|
| 1045 |
+
"dev": true,
|
| 1046 |
+
"license": "MIT",
|
| 1047 |
+
"optional": true,
|
| 1048 |
+
"os": [
|
| 1049 |
+
"linux"
|
| 1050 |
+
]
|
| 1051 |
+
},
|
| 1052 |
+
"node_modules/@rollup/rollup-linux-x64-musl": {
|
| 1053 |
+
"version": "4.58.0",
|
| 1054 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.58.0.tgz",
|
| 1055 |
+
"integrity": "sha512-ZjMyby5SICi227y1MTR3VYBpFTdZs823Rs/hpakufleBoufoOIB6jtm9FEoxn/cgO7l6PM2rCEl5Kre5vX0QrQ==",
|
| 1056 |
+
"cpu": [
|
| 1057 |
+
"x64"
|
| 1058 |
+
],
|
| 1059 |
+
"dev": true,
|
| 1060 |
+
"license": "MIT",
|
| 1061 |
+
"optional": true,
|
| 1062 |
+
"os": [
|
| 1063 |
+
"linux"
|
| 1064 |
+
]
|
| 1065 |
+
},
|
| 1066 |
+
"node_modules/@rollup/rollup-openbsd-x64": {
|
| 1067 |
+
"version": "4.58.0",
|
| 1068 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.58.0.tgz",
|
| 1069 |
+
"integrity": "sha512-ds4iwfYkSQ0k1nb8LTcyXw//ToHOnNTJtceySpL3fa7tc/AsE+UpUFphW126A6fKBGJD5dhRvg8zw1rvoGFxmw==",
|
| 1070 |
+
"cpu": [
|
| 1071 |
+
"x64"
|
| 1072 |
+
],
|
| 1073 |
+
"dev": true,
|
| 1074 |
+
"license": "MIT",
|
| 1075 |
+
"optional": true,
|
| 1076 |
+
"os": [
|
| 1077 |
+
"openbsd"
|
| 1078 |
+
]
|
| 1079 |
+
},
|
| 1080 |
+
"node_modules/@rollup/rollup-openharmony-arm64": {
|
| 1081 |
+
"version": "4.58.0",
|
| 1082 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.58.0.tgz",
|
| 1083 |
+
"integrity": "sha512-fd/zpJniln4ICdPkjWFhZYeY/bpnaN9pGa6ko+5WD38I0tTqk9lXMgXZg09MNdhpARngmxiCg0B0XUamNw/5BQ==",
|
| 1084 |
+
"cpu": [
|
| 1085 |
+
"arm64"
|
| 1086 |
+
],
|
| 1087 |
+
"dev": true,
|
| 1088 |
+
"license": "MIT",
|
| 1089 |
+
"optional": true,
|
| 1090 |
+
"os": [
|
| 1091 |
+
"openharmony"
|
| 1092 |
+
]
|
| 1093 |
+
},
|
| 1094 |
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
| 1095 |
+
"version": "4.58.0",
|
| 1096 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.58.0.tgz",
|
| 1097 |
+
"integrity": "sha512-YpG8dUOip7DCz3nr/JUfPbIUo+2d/dy++5bFzgi4ugOGBIox+qMbbqt/JoORwvI/C9Kn2tz6+Bieoqd5+B1CjA==",
|
| 1098 |
+
"cpu": [
|
| 1099 |
+
"arm64"
|
| 1100 |
+
],
|
| 1101 |
+
"dev": true,
|
| 1102 |
+
"license": "MIT",
|
| 1103 |
+
"optional": true,
|
| 1104 |
+
"os": [
|
| 1105 |
+
"win32"
|
| 1106 |
+
]
|
| 1107 |
+
},
|
| 1108 |
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
| 1109 |
+
"version": "4.58.0",
|
| 1110 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.58.0.tgz",
|
| 1111 |
+
"integrity": "sha512-b9DI8jpFQVh4hIXFr0/+N/TzLdpBIoPzjt0Rt4xJbW3mzguV3mduR9cNgiuFcuL/TeORejJhCWiAXe3E/6PxWA==",
|
| 1112 |
+
"cpu": [
|
| 1113 |
+
"ia32"
|
| 1114 |
+
],
|
| 1115 |
+
"dev": true,
|
| 1116 |
+
"license": "MIT",
|
| 1117 |
+
"optional": true,
|
| 1118 |
+
"os": [
|
| 1119 |
+
"win32"
|
| 1120 |
+
]
|
| 1121 |
+
},
|
| 1122 |
+
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
| 1123 |
+
"version": "4.58.0",
|
| 1124 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.58.0.tgz",
|
| 1125 |
+
"integrity": "sha512-CSrVpmoRJFN06LL9xhkitkwUcTZtIotYAF5p6XOR2zW0Zz5mzb3IPpcoPhB02frzMHFNo1reQ9xSF5fFm3hUsQ==",
|
| 1126 |
+
"cpu": [
|
| 1127 |
+
"x64"
|
| 1128 |
+
],
|
| 1129 |
+
"dev": true,
|
| 1130 |
+
"license": "MIT",
|
| 1131 |
+
"optional": true,
|
| 1132 |
+
"os": [
|
| 1133 |
+
"win32"
|
| 1134 |
+
]
|
| 1135 |
+
},
|
| 1136 |
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
| 1137 |
+
"version": "4.58.0",
|
| 1138 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.58.0.tgz",
|
| 1139 |
+
"integrity": "sha512-QFsBgQNTnh5K0t/sBsjJLq24YVqEIVkGpfN2VHsnN90soZyhaiA9UUHufcctVNL4ypJY0wrwad0wslx2KJQ1/w==",
|
| 1140 |
+
"cpu": [
|
| 1141 |
+
"x64"
|
| 1142 |
+
],
|
| 1143 |
+
"dev": true,
|
| 1144 |
+
"license": "MIT",
|
| 1145 |
+
"optional": true,
|
| 1146 |
+
"os": [
|
| 1147 |
+
"win32"
|
| 1148 |
+
]
|
| 1149 |
+
},
|
| 1150 |
+
"node_modules/@types/babel__core": {
|
| 1151 |
+
"version": "7.20.5",
|
| 1152 |
+
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
| 1153 |
+
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
| 1154 |
+
"dev": true,
|
| 1155 |
+
"license": "MIT",
|
| 1156 |
+
"dependencies": {
|
| 1157 |
+
"@babel/parser": "^7.20.7",
|
| 1158 |
+
"@babel/types": "^7.20.7",
|
| 1159 |
+
"@types/babel__generator": "*",
|
| 1160 |
+
"@types/babel__template": "*",
|
| 1161 |
+
"@types/babel__traverse": "*"
|
| 1162 |
+
}
|
| 1163 |
+
},
|
| 1164 |
+
"node_modules/@types/babel__generator": {
|
| 1165 |
+
"version": "7.27.0",
|
| 1166 |
+
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
| 1167 |
+
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
| 1168 |
+
"dev": true,
|
| 1169 |
+
"license": "MIT",
|
| 1170 |
+
"dependencies": {
|
| 1171 |
+
"@babel/types": "^7.0.0"
|
| 1172 |
+
}
|
| 1173 |
+
},
|
| 1174 |
+
"node_modules/@types/babel__template": {
|
| 1175 |
+
"version": "7.4.4",
|
| 1176 |
+
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
| 1177 |
+
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
| 1178 |
+
"dev": true,
|
| 1179 |
+
"license": "MIT",
|
| 1180 |
+
"dependencies": {
|
| 1181 |
+
"@babel/parser": "^7.1.0",
|
| 1182 |
+
"@babel/types": "^7.0.0"
|
| 1183 |
+
}
|
| 1184 |
+
},
|
| 1185 |
+
"node_modules/@types/babel__traverse": {
|
| 1186 |
+
"version": "7.28.0",
|
| 1187 |
+
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
| 1188 |
+
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
| 1189 |
+
"dev": true,
|
| 1190 |
+
"license": "MIT",
|
| 1191 |
+
"dependencies": {
|
| 1192 |
+
"@babel/types": "^7.28.2"
|
| 1193 |
+
}
|
| 1194 |
+
},
|
| 1195 |
+
"node_modules/@types/estree": {
|
| 1196 |
+
"version": "1.0.8",
|
| 1197 |
+
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
| 1198 |
+
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 1199 |
+
"dev": true,
|
| 1200 |
+
"license": "MIT"
|
| 1201 |
+
},
|
| 1202 |
+
"node_modules/@vitejs/plugin-react": {
|
| 1203 |
+
"version": "4.7.0",
|
| 1204 |
+
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
| 1205 |
+
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
|
| 1206 |
+
"dev": true,
|
| 1207 |
+
"license": "MIT",
|
| 1208 |
+
"dependencies": {
|
| 1209 |
+
"@babel/core": "^7.28.0",
|
| 1210 |
+
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
| 1211 |
+
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
| 1212 |
+
"@rolldown/pluginutils": "1.0.0-beta.27",
|
| 1213 |
+
"@types/babel__core": "^7.20.5",
|
| 1214 |
+
"react-refresh": "^0.17.0"
|
| 1215 |
+
},
|
| 1216 |
+
"engines": {
|
| 1217 |
+
"node": "^14.18.0 || >=16.0.0"
|
| 1218 |
+
},
|
| 1219 |
+
"peerDependencies": {
|
| 1220 |
+
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
| 1221 |
+
}
|
| 1222 |
+
},
|
| 1223 |
+
"node_modules/baseline-browser-mapping": {
|
| 1224 |
+
"version": "2.10.0",
|
| 1225 |
+
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
|
| 1226 |
+
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
|
| 1227 |
+
"dev": true,
|
| 1228 |
+
"license": "Apache-2.0",
|
| 1229 |
+
"bin": {
|
| 1230 |
+
"baseline-browser-mapping": "dist/cli.cjs"
|
| 1231 |
+
},
|
| 1232 |
+
"engines": {
|
| 1233 |
+
"node": ">=6.0.0"
|
| 1234 |
+
}
|
| 1235 |
+
},
|
| 1236 |
+
"node_modules/browserslist": {
|
| 1237 |
+
"version": "4.28.1",
|
| 1238 |
+
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
| 1239 |
+
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
| 1240 |
+
"dev": true,
|
| 1241 |
+
"funding": [
|
| 1242 |
+
{
|
| 1243 |
+
"type": "opencollective",
|
| 1244 |
+
"url": "https://opencollective.com/browserslist"
|
| 1245 |
+
},
|
| 1246 |
+
{
|
| 1247 |
+
"type": "tidelift",
|
| 1248 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 1249 |
+
},
|
| 1250 |
+
{
|
| 1251 |
+
"type": "github",
|
| 1252 |
+
"url": "https://github.com/sponsors/ai"
|
| 1253 |
+
}
|
| 1254 |
+
],
|
| 1255 |
+
"license": "MIT",
|
| 1256 |
+
"dependencies": {
|
| 1257 |
+
"baseline-browser-mapping": "^2.9.0",
|
| 1258 |
+
"caniuse-lite": "^1.0.30001759",
|
| 1259 |
+
"electron-to-chromium": "^1.5.263",
|
| 1260 |
+
"node-releases": "^2.0.27",
|
| 1261 |
+
"update-browserslist-db": "^1.2.0"
|
| 1262 |
+
},
|
| 1263 |
+
"bin": {
|
| 1264 |
+
"browserslist": "cli.js"
|
| 1265 |
+
},
|
| 1266 |
+
"engines": {
|
| 1267 |
+
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 1268 |
+
}
|
| 1269 |
+
},
|
| 1270 |
+
"node_modules/caniuse-lite": {
|
| 1271 |
+
"version": "1.0.30001770",
|
| 1272 |
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
|
| 1273 |
+
"integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==",
|
| 1274 |
+
"dev": true,
|
| 1275 |
+
"funding": [
|
| 1276 |
+
{
|
| 1277 |
+
"type": "opencollective",
|
| 1278 |
+
"url": "https://opencollective.com/browserslist"
|
| 1279 |
+
},
|
| 1280 |
+
{
|
| 1281 |
+
"type": "tidelift",
|
| 1282 |
+
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
| 1283 |
+
},
|
| 1284 |
+
{
|
| 1285 |
+
"type": "github",
|
| 1286 |
+
"url": "https://github.com/sponsors/ai"
|
| 1287 |
+
}
|
| 1288 |
+
],
|
| 1289 |
+
"license": "CC-BY-4.0"
|
| 1290 |
+
},
|
| 1291 |
+
"node_modules/convert-source-map": {
|
| 1292 |
+
"version": "2.0.0",
|
| 1293 |
+
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
| 1294 |
+
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
| 1295 |
+
"dev": true,
|
| 1296 |
+
"license": "MIT"
|
| 1297 |
+
},
|
| 1298 |
+
"node_modules/debug": {
|
| 1299 |
+
"version": "4.4.3",
|
| 1300 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 1301 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 1302 |
+
"dev": true,
|
| 1303 |
+
"license": "MIT",
|
| 1304 |
+
"dependencies": {
|
| 1305 |
+
"ms": "^2.1.3"
|
| 1306 |
+
},
|
| 1307 |
+
"engines": {
|
| 1308 |
+
"node": ">=6.0"
|
| 1309 |
+
},
|
| 1310 |
+
"peerDependenciesMeta": {
|
| 1311 |
+
"supports-color": {
|
| 1312 |
+
"optional": true
|
| 1313 |
+
}
|
| 1314 |
+
}
|
| 1315 |
+
},
|
| 1316 |
+
"node_modules/electron-to-chromium": {
|
| 1317 |
+
"version": "1.5.302",
|
| 1318 |
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
|
| 1319 |
+
"integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
|
| 1320 |
+
"dev": true,
|
| 1321 |
+
"license": "ISC"
|
| 1322 |
+
},
|
| 1323 |
+
"node_modules/esbuild": {
|
| 1324 |
+
"version": "0.25.12",
|
| 1325 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
| 1326 |
+
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
| 1327 |
+
"dev": true,
|
| 1328 |
+
"hasInstallScript": true,
|
| 1329 |
+
"license": "MIT",
|
| 1330 |
+
"bin": {
|
| 1331 |
+
"esbuild": "bin/esbuild"
|
| 1332 |
+
},
|
| 1333 |
+
"engines": {
|
| 1334 |
+
"node": ">=18"
|
| 1335 |
+
},
|
| 1336 |
+
"optionalDependencies": {
|
| 1337 |
+
"@esbuild/aix-ppc64": "0.25.12",
|
| 1338 |
+
"@esbuild/android-arm": "0.25.12",
|
| 1339 |
+
"@esbuild/android-arm64": "0.25.12",
|
| 1340 |
+
"@esbuild/android-x64": "0.25.12",
|
| 1341 |
+
"@esbuild/darwin-arm64": "0.25.12",
|
| 1342 |
+
"@esbuild/darwin-x64": "0.25.12",
|
| 1343 |
+
"@esbuild/freebsd-arm64": "0.25.12",
|
| 1344 |
+
"@esbuild/freebsd-x64": "0.25.12",
|
| 1345 |
+
"@esbuild/linux-arm": "0.25.12",
|
| 1346 |
+
"@esbuild/linux-arm64": "0.25.12",
|
| 1347 |
+
"@esbuild/linux-ia32": "0.25.12",
|
| 1348 |
+
"@esbuild/linux-loong64": "0.25.12",
|
| 1349 |
+
"@esbuild/linux-mips64el": "0.25.12",
|
| 1350 |
+
"@esbuild/linux-ppc64": "0.25.12",
|
| 1351 |
+
"@esbuild/linux-riscv64": "0.25.12",
|
| 1352 |
+
"@esbuild/linux-s390x": "0.25.12",
|
| 1353 |
+
"@esbuild/linux-x64": "0.25.12",
|
| 1354 |
+
"@esbuild/netbsd-arm64": "0.25.12",
|
| 1355 |
+
"@esbuild/netbsd-x64": "0.25.12",
|
| 1356 |
+
"@esbuild/openbsd-arm64": "0.25.12",
|
| 1357 |
+
"@esbuild/openbsd-x64": "0.25.12",
|
| 1358 |
+
"@esbuild/openharmony-arm64": "0.25.12",
|
| 1359 |
+
"@esbuild/sunos-x64": "0.25.12",
|
| 1360 |
+
"@esbuild/win32-arm64": "0.25.12",
|
| 1361 |
+
"@esbuild/win32-ia32": "0.25.12",
|
| 1362 |
+
"@esbuild/win32-x64": "0.25.12"
|
| 1363 |
+
}
|
| 1364 |
+
},
|
| 1365 |
+
"node_modules/escalade": {
|
| 1366 |
+
"version": "3.2.0",
|
| 1367 |
+
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
| 1368 |
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
| 1369 |
+
"dev": true,
|
| 1370 |
+
"license": "MIT",
|
| 1371 |
+
"engines": {
|
| 1372 |
+
"node": ">=6"
|
| 1373 |
+
}
|
| 1374 |
+
},
|
| 1375 |
+
"node_modules/fdir": {
|
| 1376 |
+
"version": "6.5.0",
|
| 1377 |
+
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
| 1378 |
+
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
| 1379 |
+
"dev": true,
|
| 1380 |
+
"license": "MIT",
|
| 1381 |
+
"engines": {
|
| 1382 |
+
"node": ">=12.0.0"
|
| 1383 |
+
},
|
| 1384 |
+
"peerDependencies": {
|
| 1385 |
+
"picomatch": "^3 || ^4"
|
| 1386 |
+
},
|
| 1387 |
+
"peerDependenciesMeta": {
|
| 1388 |
+
"picomatch": {
|
| 1389 |
+
"optional": true
|
| 1390 |
+
}
|
| 1391 |
+
}
|
| 1392 |
+
},
|
| 1393 |
+
"node_modules/fsevents": {
|
| 1394 |
+
"version": "2.3.3",
|
| 1395 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 1396 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 1397 |
+
"dev": true,
|
| 1398 |
+
"hasInstallScript": true,
|
| 1399 |
+
"license": "MIT",
|
| 1400 |
+
"optional": true,
|
| 1401 |
+
"os": [
|
| 1402 |
+
"darwin"
|
| 1403 |
+
],
|
| 1404 |
+
"engines": {
|
| 1405 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1406 |
+
}
|
| 1407 |
+
},
|
| 1408 |
+
"node_modules/gensync": {
|
| 1409 |
+
"version": "1.0.0-beta.2",
|
| 1410 |
+
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
| 1411 |
+
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
| 1412 |
+
"dev": true,
|
| 1413 |
+
"license": "MIT",
|
| 1414 |
+
"engines": {
|
| 1415 |
+
"node": ">=6.9.0"
|
| 1416 |
+
}
|
| 1417 |
+
},
|
| 1418 |
+
"node_modules/js-tokens": {
|
| 1419 |
+
"version": "4.0.0",
|
| 1420 |
+
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
| 1421 |
+
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
| 1422 |
+
"license": "MIT"
|
| 1423 |
+
},
|
| 1424 |
+
"node_modules/jsesc": {
|
| 1425 |
+
"version": "3.1.0",
|
| 1426 |
+
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
| 1427 |
+
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
| 1428 |
+
"dev": true,
|
| 1429 |
+
"license": "MIT",
|
| 1430 |
+
"bin": {
|
| 1431 |
+
"jsesc": "bin/jsesc"
|
| 1432 |
+
},
|
| 1433 |
+
"engines": {
|
| 1434 |
+
"node": ">=6"
|
| 1435 |
+
}
|
| 1436 |
+
},
|
| 1437 |
+
"node_modules/json5": {
|
| 1438 |
+
"version": "2.2.3",
|
| 1439 |
+
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
| 1440 |
+
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
| 1441 |
+
"dev": true,
|
| 1442 |
+
"license": "MIT",
|
| 1443 |
+
"bin": {
|
| 1444 |
+
"json5": "lib/cli.js"
|
| 1445 |
+
},
|
| 1446 |
+
"engines": {
|
| 1447 |
+
"node": ">=6"
|
| 1448 |
+
}
|
| 1449 |
+
},
|
| 1450 |
+
"node_modules/loose-envify": {
|
| 1451 |
+
"version": "1.4.0",
|
| 1452 |
+
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
| 1453 |
+
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
| 1454 |
+
"license": "MIT",
|
| 1455 |
+
"dependencies": {
|
| 1456 |
+
"js-tokens": "^3.0.0 || ^4.0.0"
|
| 1457 |
+
},
|
| 1458 |
+
"bin": {
|
| 1459 |
+
"loose-envify": "cli.js"
|
| 1460 |
+
}
|
| 1461 |
+
},
|
| 1462 |
+
"node_modules/lru-cache": {
|
| 1463 |
+
"version": "5.1.1",
|
| 1464 |
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
| 1465 |
+
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
| 1466 |
+
"dev": true,
|
| 1467 |
+
"license": "ISC",
|
| 1468 |
+
"dependencies": {
|
| 1469 |
+
"yallist": "^3.0.2"
|
| 1470 |
+
}
|
| 1471 |
+
},
|
| 1472 |
+
"node_modules/ms": {
|
| 1473 |
+
"version": "2.1.3",
|
| 1474 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 1475 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 1476 |
+
"dev": true,
|
| 1477 |
+
"license": "MIT"
|
| 1478 |
+
},
|
| 1479 |
+
"node_modules/nanoid": {
|
| 1480 |
+
"version": "3.3.11",
|
| 1481 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
| 1482 |
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
| 1483 |
+
"dev": true,
|
| 1484 |
+
"funding": [
|
| 1485 |
+
{
|
| 1486 |
+
"type": "github",
|
| 1487 |
+
"url": "https://github.com/sponsors/ai"
|
| 1488 |
+
}
|
| 1489 |
+
],
|
| 1490 |
+
"license": "MIT",
|
| 1491 |
+
"bin": {
|
| 1492 |
+
"nanoid": "bin/nanoid.cjs"
|
| 1493 |
+
},
|
| 1494 |
+
"engines": {
|
| 1495 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 1496 |
+
}
|
| 1497 |
+
},
|
| 1498 |
+
"node_modules/node-releases": {
|
| 1499 |
+
"version": "2.0.27",
|
| 1500 |
+
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
| 1501 |
+
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
| 1502 |
+
"dev": true,
|
| 1503 |
+
"license": "MIT"
|
| 1504 |
+
},
|
| 1505 |
+
"node_modules/picocolors": {
|
| 1506 |
+
"version": "1.1.1",
|
| 1507 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 1508 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 1509 |
+
"dev": true,
|
| 1510 |
+
"license": "ISC"
|
| 1511 |
+
},
|
| 1512 |
+
"node_modules/picomatch": {
|
| 1513 |
+
"version": "4.0.3",
|
| 1514 |
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
| 1515 |
+
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
| 1516 |
+
"dev": true,
|
| 1517 |
+
"license": "MIT",
|
| 1518 |
+
"engines": {
|
| 1519 |
+
"node": ">=12"
|
| 1520 |
+
},
|
| 1521 |
+
"funding": {
|
| 1522 |
+
"url": "https://github.com/sponsors/jonschlinkert"
|
| 1523 |
+
}
|
| 1524 |
+
},
|
| 1525 |
+
"node_modules/postcss": {
|
| 1526 |
+
"version": "8.5.6",
|
| 1527 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
| 1528 |
+
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
| 1529 |
+
"dev": true,
|
| 1530 |
+
"funding": [
|
| 1531 |
+
{
|
| 1532 |
+
"type": "opencollective",
|
| 1533 |
+
"url": "https://opencollective.com/postcss/"
|
| 1534 |
+
},
|
| 1535 |
+
{
|
| 1536 |
+
"type": "tidelift",
|
| 1537 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 1538 |
+
},
|
| 1539 |
+
{
|
| 1540 |
+
"type": "github",
|
| 1541 |
+
"url": "https://github.com/sponsors/ai"
|
| 1542 |
+
}
|
| 1543 |
+
],
|
| 1544 |
+
"license": "MIT",
|
| 1545 |
+
"dependencies": {
|
| 1546 |
+
"nanoid": "^3.3.11",
|
| 1547 |
+
"picocolors": "^1.1.1",
|
| 1548 |
+
"source-map-js": "^1.2.1"
|
| 1549 |
+
},
|
| 1550 |
+
"engines": {
|
| 1551 |
+
"node": "^10 || ^12 || >=14"
|
| 1552 |
+
}
|
| 1553 |
+
},
|
| 1554 |
+
"node_modules/react": {
|
| 1555 |
+
"version": "18.3.1",
|
| 1556 |
+
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
| 1557 |
+
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
| 1558 |
+
"license": "MIT",
|
| 1559 |
+
"dependencies": {
|
| 1560 |
+
"loose-envify": "^1.1.0"
|
| 1561 |
+
},
|
| 1562 |
+
"engines": {
|
| 1563 |
+
"node": ">=0.10.0"
|
| 1564 |
+
}
|
| 1565 |
+
},
|
| 1566 |
+
"node_modules/react-dom": {
|
| 1567 |
+
"version": "18.3.1",
|
| 1568 |
+
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
| 1569 |
+
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
| 1570 |
+
"license": "MIT",
|
| 1571 |
+
"dependencies": {
|
| 1572 |
+
"loose-envify": "^1.1.0",
|
| 1573 |
+
"scheduler": "^0.23.2"
|
| 1574 |
+
},
|
| 1575 |
+
"peerDependencies": {
|
| 1576 |
+
"react": "^18.3.1"
|
| 1577 |
+
}
|
| 1578 |
+
},
|
| 1579 |
+
"node_modules/react-refresh": {
|
| 1580 |
+
"version": "0.17.0",
|
| 1581 |
+
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
| 1582 |
+
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
| 1583 |
+
"dev": true,
|
| 1584 |
+
"license": "MIT",
|
| 1585 |
+
"engines": {
|
| 1586 |
+
"node": ">=0.10.0"
|
| 1587 |
+
}
|
| 1588 |
+
},
|
| 1589 |
+
"node_modules/rollup": {
|
| 1590 |
+
"version": "4.58.0",
|
| 1591 |
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.58.0.tgz",
|
| 1592 |
+
"integrity": "sha512-wbT0mBmWbIvvq8NeEYWWvevvxnOyhKChir47S66WCxw1SXqhw7ssIYejnQEVt7XYQpsj2y8F9PM+Cr3SNEa0gw==",
|
| 1593 |
+
"dev": true,
|
| 1594 |
+
"license": "MIT",
|
| 1595 |
+
"dependencies": {
|
| 1596 |
+
"@types/estree": "1.0.8"
|
| 1597 |
+
},
|
| 1598 |
+
"bin": {
|
| 1599 |
+
"rollup": "dist/bin/rollup"
|
| 1600 |
+
},
|
| 1601 |
+
"engines": {
|
| 1602 |
+
"node": ">=18.0.0",
|
| 1603 |
+
"npm": ">=8.0.0"
|
| 1604 |
+
},
|
| 1605 |
+
"optionalDependencies": {
|
| 1606 |
+
"@rollup/rollup-android-arm-eabi": "4.58.0",
|
| 1607 |
+
"@rollup/rollup-android-arm64": "4.58.0",
|
| 1608 |
+
"@rollup/rollup-darwin-arm64": "4.58.0",
|
| 1609 |
+
"@rollup/rollup-darwin-x64": "4.58.0",
|
| 1610 |
+
"@rollup/rollup-freebsd-arm64": "4.58.0",
|
| 1611 |
+
"@rollup/rollup-freebsd-x64": "4.58.0",
|
| 1612 |
+
"@rollup/rollup-linux-arm-gnueabihf": "4.58.0",
|
| 1613 |
+
"@rollup/rollup-linux-arm-musleabihf": "4.58.0",
|
| 1614 |
+
"@rollup/rollup-linux-arm64-gnu": "4.58.0",
|
| 1615 |
+
"@rollup/rollup-linux-arm64-musl": "4.58.0",
|
| 1616 |
+
"@rollup/rollup-linux-loong64-gnu": "4.58.0",
|
| 1617 |
+
"@rollup/rollup-linux-loong64-musl": "4.58.0",
|
| 1618 |
+
"@rollup/rollup-linux-ppc64-gnu": "4.58.0",
|
| 1619 |
+
"@rollup/rollup-linux-ppc64-musl": "4.58.0",
|
| 1620 |
+
"@rollup/rollup-linux-riscv64-gnu": "4.58.0",
|
| 1621 |
+
"@rollup/rollup-linux-riscv64-musl": "4.58.0",
|
| 1622 |
+
"@rollup/rollup-linux-s390x-gnu": "4.58.0",
|
| 1623 |
+
"@rollup/rollup-linux-x64-gnu": "4.58.0",
|
| 1624 |
+
"@rollup/rollup-linux-x64-musl": "4.58.0",
|
| 1625 |
+
"@rollup/rollup-openbsd-x64": "4.58.0",
|
| 1626 |
+
"@rollup/rollup-openharmony-arm64": "4.58.0",
|
| 1627 |
+
"@rollup/rollup-win32-arm64-msvc": "4.58.0",
|
| 1628 |
+
"@rollup/rollup-win32-ia32-msvc": "4.58.0",
|
| 1629 |
+
"@rollup/rollup-win32-x64-gnu": "4.58.0",
|
| 1630 |
+
"@rollup/rollup-win32-x64-msvc": "4.58.0",
|
| 1631 |
+
"fsevents": "~2.3.2"
|
| 1632 |
+
}
|
| 1633 |
+
},
|
| 1634 |
+
"node_modules/scheduler": {
|
| 1635 |
+
"version": "0.23.2",
|
| 1636 |
+
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
| 1637 |
+
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
| 1638 |
+
"license": "MIT",
|
| 1639 |
+
"dependencies": {
|
| 1640 |
+
"loose-envify": "^1.1.0"
|
| 1641 |
+
}
|
| 1642 |
+
},
|
| 1643 |
+
"node_modules/semver": {
|
| 1644 |
+
"version": "6.3.1",
|
| 1645 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
| 1646 |
+
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
| 1647 |
+
"dev": true,
|
| 1648 |
+
"license": "ISC",
|
| 1649 |
+
"bin": {
|
| 1650 |
+
"semver": "bin/semver.js"
|
| 1651 |
+
}
|
| 1652 |
+
},
|
| 1653 |
+
"node_modules/source-map-js": {
|
| 1654 |
+
"version": "1.2.1",
|
| 1655 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 1656 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 1657 |
+
"dev": true,
|
| 1658 |
+
"license": "BSD-3-Clause",
|
| 1659 |
+
"engines": {
|
| 1660 |
+
"node": ">=0.10.0"
|
| 1661 |
+
}
|
| 1662 |
+
},
|
| 1663 |
+
"node_modules/tinyglobby": {
|
| 1664 |
+
"version": "0.2.15",
|
| 1665 |
+
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
| 1666 |
+
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
| 1667 |
+
"dev": true,
|
| 1668 |
+
"license": "MIT",
|
| 1669 |
+
"dependencies": {
|
| 1670 |
+
"fdir": "^6.5.0",
|
| 1671 |
+
"picomatch": "^4.0.3"
|
| 1672 |
+
},
|
| 1673 |
+
"engines": {
|
| 1674 |
+
"node": ">=12.0.0"
|
| 1675 |
+
},
|
| 1676 |
+
"funding": {
|
| 1677 |
+
"url": "https://github.com/sponsors/SuperchupuDev"
|
| 1678 |
+
}
|
| 1679 |
+
},
|
| 1680 |
+
"node_modules/update-browserslist-db": {
|
| 1681 |
+
"version": "1.2.3",
|
| 1682 |
+
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
| 1683 |
+
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
| 1684 |
+
"dev": true,
|
| 1685 |
+
"funding": [
|
| 1686 |
+
{
|
| 1687 |
+
"type": "opencollective",
|
| 1688 |
+
"url": "https://opencollective.com/browserslist"
|
| 1689 |
+
},
|
| 1690 |
+
{
|
| 1691 |
+
"type": "tidelift",
|
| 1692 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 1693 |
+
},
|
| 1694 |
+
{
|
| 1695 |
+
"type": "github",
|
| 1696 |
+
"url": "https://github.com/sponsors/ai"
|
| 1697 |
+
}
|
| 1698 |
+
],
|
| 1699 |
+
"license": "MIT",
|
| 1700 |
+
"dependencies": {
|
| 1701 |
+
"escalade": "^3.2.0",
|
| 1702 |
+
"picocolors": "^1.1.1"
|
| 1703 |
+
},
|
| 1704 |
+
"bin": {
|
| 1705 |
+
"update-browserslist-db": "cli.js"
|
| 1706 |
+
},
|
| 1707 |
+
"peerDependencies": {
|
| 1708 |
+
"browserslist": ">= 4.21.0"
|
| 1709 |
+
}
|
| 1710 |
+
},
|
| 1711 |
+
"node_modules/vite": {
|
| 1712 |
+
"version": "6.4.1",
|
| 1713 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
| 1714 |
+
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
| 1715 |
+
"dev": true,
|
| 1716 |
+
"license": "MIT",
|
| 1717 |
+
"dependencies": {
|
| 1718 |
+
"esbuild": "^0.25.0",
|
| 1719 |
+
"fdir": "^6.4.4",
|
| 1720 |
+
"picomatch": "^4.0.2",
|
| 1721 |
+
"postcss": "^8.5.3",
|
| 1722 |
+
"rollup": "^4.34.9",
|
| 1723 |
+
"tinyglobby": "^0.2.13"
|
| 1724 |
+
},
|
| 1725 |
+
"bin": {
|
| 1726 |
+
"vite": "bin/vite.js"
|
| 1727 |
+
},
|
| 1728 |
+
"engines": {
|
| 1729 |
+
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
| 1730 |
+
},
|
| 1731 |
+
"funding": {
|
| 1732 |
+
"url": "https://github.com/vitejs/vite?sponsor=1"
|
| 1733 |
+
},
|
| 1734 |
+
"optionalDependencies": {
|
| 1735 |
+
"fsevents": "~2.3.3"
|
| 1736 |
+
},
|
| 1737 |
+
"peerDependencies": {
|
| 1738 |
+
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
| 1739 |
+
"jiti": ">=1.21.0",
|
| 1740 |
+
"less": "*",
|
| 1741 |
+
"lightningcss": "^1.21.0",
|
| 1742 |
+
"sass": "*",
|
| 1743 |
+
"sass-embedded": "*",
|
| 1744 |
+
"stylus": "*",
|
| 1745 |
+
"sugarss": "*",
|
| 1746 |
+
"terser": "^5.16.0",
|
| 1747 |
+
"tsx": "^4.8.1",
|
| 1748 |
+
"yaml": "^2.4.2"
|
| 1749 |
+
},
|
| 1750 |
+
"peerDependenciesMeta": {
|
| 1751 |
+
"@types/node": {
|
| 1752 |
+
"optional": true
|
| 1753 |
+
},
|
| 1754 |
+
"jiti": {
|
| 1755 |
+
"optional": true
|
| 1756 |
+
},
|
| 1757 |
+
"less": {
|
| 1758 |
+
"optional": true
|
| 1759 |
+
},
|
| 1760 |
+
"lightningcss": {
|
| 1761 |
+
"optional": true
|
| 1762 |
+
},
|
| 1763 |
+
"sass": {
|
| 1764 |
+
"optional": true
|
| 1765 |
+
},
|
| 1766 |
+
"sass-embedded": {
|
| 1767 |
+
"optional": true
|
| 1768 |
+
},
|
| 1769 |
+
"stylus": {
|
| 1770 |
+
"optional": true
|
| 1771 |
+
},
|
| 1772 |
+
"sugarss": {
|
| 1773 |
+
"optional": true
|
| 1774 |
+
},
|
| 1775 |
+
"terser": {
|
| 1776 |
+
"optional": true
|
| 1777 |
+
},
|
| 1778 |
+
"tsx": {
|
| 1779 |
+
"optional": true
|
| 1780 |
+
},
|
| 1781 |
+
"yaml": {
|
| 1782 |
+
"optional": true
|
| 1783 |
+
}
|
| 1784 |
+
}
|
| 1785 |
+
},
|
| 1786 |
+
"node_modules/yallist": {
|
| 1787 |
+
"version": "3.1.1",
|
| 1788 |
+
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
| 1789 |
+
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
| 1790 |
+
"dev": true,
|
| 1791 |
+
"license": "ISC"
|
| 1792 |
+
}
|
| 1793 |
+
}
|
| 1794 |
+
}
|
client/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "adaptiveauth-ui",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"react": "^18.3.1",
|
| 13 |
+
"react-dom": "^18.3.1"
|
| 14 |
+
},
|
| 15 |
+
"devDependencies": {
|
| 16 |
+
"@vitejs/plugin-react": "^4.3.4",
|
| 17 |
+
"vite": "^6.0.0"
|
| 18 |
+
}
|
| 19 |
+
}
|
client/src/App.jsx
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react';
|
| 2 |
+
import Topbar from './components/Topbar';
|
| 3 |
+
import TokenBar from './components/TokenBar';
|
| 4 |
+
import Scenario1Tab from './tabs/Scenario1Tab';
|
| 5 |
+
import Scenario2Tab from './tabs/Scenario2Tab';
|
| 6 |
+
import ApiTab from './tabs/ApiTab';
|
| 7 |
+
import AdminTab from './tabs/AdminTab';
|
| 8 |
+
import IntelTab from './tabs/IntelTab';
|
| 9 |
+
import { getToken, clearToken } from './api';
|
| 10 |
+
|
| 11 |
+
const TABS = [
|
| 12 |
+
{ id: 'scenario1', label: '🔬 Scenario 1: Behavior' },
|
| 13 |
+
{ id: 'scenario2', label: '🚨 Scenario 2: Attacks' },
|
| 14 |
+
{ id: 'api', label: '🔧 API Testing' },
|
| 15 |
+
{ id: 'admin', label: '🛡️ Admin Dashboard' },
|
| 16 |
+
{ id: 'intel', label: '🧠 Intelligence' },
|
| 17 |
+
];
|
| 18 |
+
|
| 19 |
+
export default function App() {
|
| 20 |
+
const [activeTab, setActiveTab] = useState('scenario1');
|
| 21 |
+
const [token, setToken] = useState(getToken);
|
| 22 |
+
const [serverStatus, setServerStatus] = useState('checking');
|
| 23 |
+
|
| 24 |
+
const refreshToken = () => setToken(getToken());
|
| 25 |
+
const handleClear = () => { clearToken(); setToken(''); };
|
| 26 |
+
|
| 27 |
+
const ping = async () => {
|
| 28 |
+
setServerStatus('checking');
|
| 29 |
+
try {
|
| 30 |
+
const r = await fetch('/health');
|
| 31 |
+
setServerStatus(r.ok ? 'online' : 'error');
|
| 32 |
+
} catch {
|
| 33 |
+
setServerStatus('offline');
|
| 34 |
+
}
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
useEffect(() => { ping(); }, []);
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<div className="app">
|
| 41 |
+
<Topbar serverStatus={serverStatus} onRefresh={ping} />
|
| 42 |
+
|
| 43 |
+
<div className="container">
|
| 44 |
+
<div className="hero">
|
| 45 |
+
<h1>Adaptive Authentication <span>Framework</span></h1>
|
| 46 |
+
<p>
|
| 47 |
+
Production-ready risk-based auth — JWT • 2FA •
|
| 48 |
+
Behavioral Analysis • Anomaly Detection
|
| 49 |
+
</p>
|
| 50 |
+
</div>
|
| 51 |
+
|
| 52 |
+
<TokenBar token={token} onRefresh={refreshToken} onClear={handleClear} />
|
| 53 |
+
|
| 54 |
+
<nav className="main-tabs" role="tablist">
|
| 55 |
+
{TABS.map(t => (
|
| 56 |
+
<button
|
| 57 |
+
key={t.id}
|
| 58 |
+
role="tab"
|
| 59 |
+
aria-selected={activeTab === t.id}
|
| 60 |
+
className={activeTab === t.id ? 'active' : ''}
|
| 61 |
+
onClick={() => setActiveTab(t.id)}
|
| 62 |
+
>
|
| 63 |
+
{t.label}
|
| 64 |
+
</button>
|
| 65 |
+
))}
|
| 66 |
+
</nav>
|
| 67 |
+
|
| 68 |
+
<div role="tabpanel">
|
| 69 |
+
{activeTab === 'scenario1' && <Scenario1Tab onTokenSave={refreshToken} />}
|
| 70 |
+
{activeTab === 'scenario2' && <Scenario2Tab onTokenSave={refreshToken} />}
|
| 71 |
+
{activeTab === 'api' && <ApiTab onTokenSave={refreshToken} />}
|
| 72 |
+
{activeTab === 'admin' && <AdminTab onTokenSave={refreshToken} />}
|
| 73 |
+
{activeTab === 'intel' && <IntelTab onTokenSave={refreshToken} />}
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
);
|
| 78 |
+
}
|
client/src/api.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Shared API utility for AdaptiveAuth React UI
|
| 2 |
+
const BASE = '/api/v1';
|
| 3 |
+
|
| 4 |
+
export const ENDPOINTS = {
|
| 5 |
+
AUTH: `${BASE}/auth`,
|
| 6 |
+
DEMO: `${BASE}/demo`,
|
| 7 |
+
ADMIN: `${BASE}/admin`,
|
| 8 |
+
USER: `${BASE}/user`,
|
| 9 |
+
RISK: `${BASE}/risk`,
|
| 10 |
+
INTEL: `${BASE}/session-intel`,
|
| 11 |
+
API: BASE,
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
export const getToken = () => localStorage.getItem('token') || '';
|
| 15 |
+
export const saveToken = (t) => localStorage.setItem('token', t);
|
| 16 |
+
export const clearToken = () => localStorage.removeItem('token');
|
| 17 |
+
|
| 18 |
+
export async function req(url, method = 'GET', body = null, auth = true) {
|
| 19 |
+
const headers = { 'Content-Type': 'application/json' };
|
| 20 |
+
if (auth) {
|
| 21 |
+
const t = getToken();
|
| 22 |
+
if (t) headers['Authorization'] = `Bearer ${t}`;
|
| 23 |
+
}
|
| 24 |
+
try {
|
| 25 |
+
const res = await fetch(url, {
|
| 26 |
+
method,
|
| 27 |
+
headers,
|
| 28 |
+
body: body != null ? JSON.stringify(body) : undefined,
|
| 29 |
+
});
|
| 30 |
+
const data = await res.json().catch(() => ({}));
|
| 31 |
+
return { ok: res.ok, status: res.status, data };
|
| 32 |
+
} catch (e) {
|
| 33 |
+
return { ok: false, status: 0, data: { detail: e.message } };
|
| 34 |
+
}
|
| 35 |
+
}
|
client/src/components/RiskViz.jsx
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ─── Risk Visualization Components ───────────────────────────────────────────
|
| 2 |
+
|
| 3 |
+
const ARC_LEN = 220;
|
| 4 |
+
|
| 5 |
+
const RISK_COLORS = {
|
| 6 |
+
low: '#16a34a',
|
| 7 |
+
medium: '#d97706',
|
| 8 |
+
high: '#f97316',
|
| 9 |
+
critical: '#dc2626',
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
const STATUS_CLASS = {
|
| 13 |
+
success: 'success',
|
| 14 |
+
challenge_required:'challenge',
|
| 15 |
+
blocked: 'blocked',
|
| 16 |
+
};
|
| 17 |
+
const STATUS_LABEL = {
|
| 18 |
+
success: '✅ ACCESS GRANTED',
|
| 19 |
+
challenge_required:'⚠️ CHALLENGE REQUIRED',
|
| 20 |
+
blocked: '🚫 BLOCKED',
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
// ── Gauge Arc (SVG Semi-circle) ───────────────────────────────────────────────
|
| 24 |
+
function GaugeArc({ score, color }) {
|
| 25 |
+
const offset = ARC_LEN - (score / 100) * ARC_LEN;
|
| 26 |
+
return (
|
| 27 |
+
<div className="gauge-wrap">
|
| 28 |
+
<svg width="170" height="100" viewBox="0 0 170 100" style={{ overflow: 'visible' }}>
|
| 29 |
+
{/* Track */}
|
| 30 |
+
<path
|
| 31 |
+
d="M 10,80 A 75,75 0 0,1 160,80"
|
| 32 |
+
fill="none"
|
| 33 |
+
stroke="#e2e8f0"
|
| 34 |
+
strokeWidth="13"
|
| 35 |
+
strokeLinecap="round"
|
| 36 |
+
/>
|
| 37 |
+
{/* Fill */}
|
| 38 |
+
<path
|
| 39 |
+
d="M 10,80 A 75,75 0 0,1 160,80"
|
| 40 |
+
fill="none"
|
| 41 |
+
stroke={color}
|
| 42 |
+
strokeWidth="13"
|
| 43 |
+
strokeLinecap="round"
|
| 44 |
+
strokeDasharray={ARC_LEN}
|
| 45 |
+
strokeDashoffset={offset}
|
| 46 |
+
style={{ transition: 'stroke-dashoffset .8s ease, stroke .5s' }}
|
| 47 |
+
/>
|
| 48 |
+
{/* Score text */}
|
| 49 |
+
<text
|
| 50 |
+
x="85"
|
| 51 |
+
y="72"
|
| 52 |
+
textAnchor="middle"
|
| 53 |
+
fontSize="28"
|
| 54 |
+
fontWeight="800"
|
| 55 |
+
fill={color}
|
| 56 |
+
style={{ transition: 'fill .5s' }}
|
| 57 |
+
>
|
| 58 |
+
{Math.round(score)}
|
| 59 |
+
</text>
|
| 60 |
+
</svg>
|
| 61 |
+
</div>
|
| 62 |
+
);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// ── Security Level Segments ───────────────────────────────────────────────────
|
| 66 |
+
function LevelBar({ level }) {
|
| 67 |
+
return (
|
| 68 |
+
<div>
|
| 69 |
+
<div className="text-sm text-muted mb-1">Security Level</div>
|
| 70 |
+
<div className="level-bar">
|
| 71 |
+
{[0, 1, 2, 3, 4].map(i => (
|
| 72 |
+
<div
|
| 73 |
+
key={i}
|
| 74 |
+
className={`level-seg ${i <= level ? `seg-${i}` : ''}`}
|
| 75 |
+
/>
|
| 76 |
+
))}
|
| 77 |
+
</div>
|
| 78 |
+
<div className="flex justify-between text-xs text-muted mt-1">
|
| 79 |
+
<span>0 Trusted</span>
|
| 80 |
+
<span>4 Blocked</span>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// ── Risk Factor Bars ──────────────────────────────────────────────────────────
|
| 87 |
+
const FACTOR_NAMES = { device: 'Device', location: 'Location', time: 'Time Pattern', velocity: 'Velocity', behavior: 'Behavior' };
|
| 88 |
+
const FACTOR_WEIGHTS = { device: '21%', location: '97.68%', time: '0.02%', velocity: '2.08%', behavior: '0.01%' };
|
| 89 |
+
|
| 90 |
+
function FactorBars({ factors }) {
|
| 91 |
+
if (!factors) return null;
|
| 92 |
+
return (
|
| 93 |
+
<div>
|
| 94 |
+
<div className="text-xs font-600 uppercase letter-wide text-muted mb-2">Risk Factor Breakdown</div>
|
| 95 |
+
{['device', 'location', 'time', 'velocity', 'behavior'].map(k => {
|
| 96 |
+
const val = factors[k] || 0;
|
| 97 |
+
const color = val > 70 ? '#dc2626' : val > 40 ? '#d97706' : '#16a34a';
|
| 98 |
+
return (
|
| 99 |
+
<div className="factor-row" key={k}>
|
| 100 |
+
<div className="factor-label">
|
| 101 |
+
<span>{FACTOR_NAMES[k]}</span>
|
| 102 |
+
<span className="text-xs">{val.toFixed(1)} / 100 (w: {FACTOR_WEIGHTS[k]})</span>
|
| 103 |
+
</div>
|
| 104 |
+
<div className="factor-bar-wrap">
|
| 105 |
+
<div className="factor-bar" style={{ width: `${val}%`, background: color }} />
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
);
|
| 109 |
+
})}
|
| 110 |
+
</div>
|
| 111 |
+
);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// ── Decision Panel ────────────────────────────────────────────────────────────
|
| 115 |
+
function DecisionPanel({ decision }) {
|
| 116 |
+
const status = decision.status || 'success';
|
| 117 |
+
const cls = STATUS_CLASS[status] || 'success';
|
| 118 |
+
const label = STATUS_LABEL[status] || status.toUpperCase();
|
| 119 |
+
const level = decision.security_level !== undefined ? decision.security_level : 0;
|
| 120 |
+
|
| 121 |
+
return (
|
| 122 |
+
<div className="decision-panel">
|
| 123 |
+
<div className={`decision-header ${cls}`}>{label}</div>
|
| 124 |
+
<div className="decision-body">
|
| 125 |
+
<div className="mb-1">
|
| 126 |
+
Security Level: <strong>{level}</strong> / 4
|
| 127 |
+
</div>
|
| 128 |
+
{decision.challenge_type && (
|
| 129 |
+
<div>Challenge: <strong>{decision.challenge_type.toUpperCase()}</strong></div>
|
| 130 |
+
)}
|
| 131 |
+
{decision.message && (
|
| 132 |
+
<div className="mt-2 text-muted text-sm">{decision.message}</div>
|
| 133 |
+
)}
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// ── Full Risk Viz Card ────────────────────────────────────────────────────────
|
| 140 |
+
export function RiskVizCard({ decision, notes }) {
|
| 141 |
+
if (!decision) return null;
|
| 142 |
+
const score = decision.risk_score || 0;
|
| 143 |
+
const level = decision.security_level !== undefined ? decision.security_level : 0;
|
| 144 |
+
const rl = (decision.risk_level || 'low').toLowerCase();
|
| 145 |
+
const color = RISK_COLORS[rl] || '#2563eb';
|
| 146 |
+
|
| 147 |
+
return (
|
| 148 |
+
<div className="card" style={{ animation: 'fadeIn .3s ease' }}>
|
| 149 |
+
<div className="card-header">📊 Risk Assessment Result</div>
|
| 150 |
+
|
| 151 |
+
<div className="grid-2" style={{ alignItems: 'start' }}>
|
| 152 |
+
<div>
|
| 153 |
+
<GaugeArc score={score} color={color} />
|
| 154 |
+
<div className="gauge-label" style={{ color }}>{rl.toUpperCase()} RISK</div>
|
| 155 |
+
</div>
|
| 156 |
+
<div>
|
| 157 |
+
<LevelBar level={level} />
|
| 158 |
+
<DecisionPanel decision={decision} />
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
<div className="mt-4">
|
| 163 |
+
<FactorBars factors={decision.risk_factors} />
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
+
{notes && notes.length > 0 && (
|
| 167 |
+
<div className="mt-3">
|
| 168 |
+
<div className="text-xs font-600 uppercase letter-wide text-muted mb-2">
|
| 169 |
+
What the Framework Saw
|
| 170 |
+
</div>
|
| 171 |
+
{notes.map((n, i) => (
|
| 172 |
+
<div
|
| 173 |
+
key={i}
|
| 174 |
+
className="text-sm text-2"
|
| 175 |
+
style={{ padding: '4px 0', borderBottom: '1px solid var(--border)' }}
|
| 176 |
+
>
|
| 177 |
+
{n}
|
| 178 |
+
</div>
|
| 179 |
+
))}
|
| 180 |
+
</div>
|
| 181 |
+
)}
|
| 182 |
+
</div>
|
| 183 |
+
);
|
| 184 |
+
}
|
client/src/components/TokenBar.jsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default function TokenBar({ token, onRefresh, onClear }) {
|
| 2 |
+
const handleCopy = () => {
|
| 3 |
+
if (!token) { alert('No token to copy.'); return; }
|
| 4 |
+
navigator.clipboard
|
| 5 |
+
.writeText(token)
|
| 6 |
+
.then(() => alert('Token copied!'))
|
| 7 |
+
.catch(() => alert(token.substring(0, 80) + '…'));
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
return (
|
| 11 |
+
<div className="token-bar">
|
| 12 |
+
<span className="token-label">JWT Token</span>
|
| 13 |
+
<span className="token-val">
|
| 14 |
+
{token ? token.substring(0, 64) + '…' : 'No token — log in first'}
|
| 15 |
+
</span>
|
| 16 |
+
<button className="btn btn-ghost btn-sm" onClick={handleCopy}>Copy</button>
|
| 17 |
+
<button className="btn btn-danger btn-sm" onClick={onClear}>Clear</button>
|
| 18 |
+
</div>
|
| 19 |
+
);
|
| 20 |
+
}
|
client/src/components/Topbar.jsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default function Topbar({ serverStatus, onRefresh }) {
|
| 2 |
+
const statusText =
|
| 3 |
+
serverStatus === 'online' ? 'Server online' :
|
| 4 |
+
serverStatus === 'checking' ? 'Checking…' :
|
| 5 |
+
serverStatus === 'error' ? 'Server error' : 'Server offline';
|
| 6 |
+
|
| 7 |
+
return (
|
| 8 |
+
<header className="topbar">
|
| 9 |
+
<div className="topbar-logo">
|
| 10 |
+
⚡ Adaptive<em>Auth</em>
|
| 11 |
+
</div>
|
| 12 |
+
<div className="topbar-status">
|
| 13 |
+
<span className={`status-dot ${serverStatus}`}></span>
|
| 14 |
+
<span>{statusText}</span>
|
| 15 |
+
<button className="btn btn-ghost btn-sm" onClick={onRefresh} style={{ marginLeft: 4 }}>
|
| 16 |
+
Refresh
|
| 17 |
+
</button>
|
| 18 |
+
</div>
|
| 19 |
+
</header>
|
| 20 |
+
);
|
| 21 |
+
}
|
client/src/components/ui.jsx
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ─── Shared UI Primitives ────────────────────────────────────────────────────
|
| 2 |
+
|
| 3 |
+
export function Card({ children, style }) {
|
| 4 |
+
return <div className="card" style={style}>{children}</div>;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export function CardHeader({ icon, children, actions }) {
|
| 8 |
+
if (actions) {
|
| 9 |
+
return (
|
| 10 |
+
<div className="card-header-row">
|
| 11 |
+
<span className="card-title">
|
| 12 |
+
{icon && <span>{icon}</span>}
|
| 13 |
+
{children}
|
| 14 |
+
</span>
|
| 15 |
+
{actions}
|
| 16 |
+
</div>
|
| 17 |
+
);
|
| 18 |
+
}
|
| 19 |
+
return (
|
| 20 |
+
<div className="card-header">
|
| 21 |
+
{icon && <span>{icon}</span>}
|
| 22 |
+
{children}
|
| 23 |
+
</div>
|
| 24 |
+
);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export function Callout({ type = 'info', children }) {
|
| 28 |
+
return <div className={`callout callout-${type}`}>{children}</div>;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export function FormGroup({ label, children }) {
|
| 32 |
+
return (
|
| 33 |
+
<div className="form-group">
|
| 34 |
+
{label && <label>{label}</label>}
|
| 35 |
+
{children}
|
| 36 |
+
</div>
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export function ResponseBox({ result }) {
|
| 41 |
+
if (!result) return null;
|
| 42 |
+
return (
|
| 43 |
+
<div className={`resp-box ${result.ok ? 'ok' : 'err'}`}>
|
| 44 |
+
{JSON.stringify(result.data, null, 2)}
|
| 45 |
+
</div>
|
| 46 |
+
);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
export function Tag({ children }) {
|
| 50 |
+
return <span className="tag">{children}</span>;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
export function Badge({ type = 'muted', children }) {
|
| 54 |
+
return <span className={`badge badge-${type}`}>{children}</span>;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
export function StepBar({ steps, current }) {
|
| 58 |
+
return (
|
| 59 |
+
<div className="step-bar">
|
| 60 |
+
{steps.map((s, i) => (
|
| 61 |
+
<div
|
| 62 |
+
key={i}
|
| 63 |
+
className={`step-item ${i < current ? 'done' : i === current ? 'active' : ''}`}
|
| 64 |
+
>
|
| 65 |
+
<div className="step-num">{i < current ? '✓' : i}</div>
|
| 66 |
+
<div className="step-label">{s.label}</div>
|
| 67 |
+
<div className="step-sub">{s.sub}</div>
|
| 68 |
+
</div>
|
| 69 |
+
))}
|
| 70 |
+
</div>
|
| 71 |
+
);
|
| 72 |
+
}
|
client/src/index.css
ADDED
|
@@ -0,0 +1,707 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* =====================================================
|
| 2 |
+
AdaptiveAuth UI — Professional Light Theme
|
| 3 |
+
===================================================== */
|
| 4 |
+
|
| 5 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 6 |
+
|
| 7 |
+
:root {
|
| 8 |
+
/* Background */
|
| 9 |
+
--bg: #f0f4f8;
|
| 10 |
+
--surface: #ffffff;
|
| 11 |
+
--surface-2: #f8fafc;
|
| 12 |
+
|
| 13 |
+
/* Borders */
|
| 14 |
+
--border: #e2e8f0;
|
| 15 |
+
--border-2: #cbd5e1;
|
| 16 |
+
|
| 17 |
+
/* Text */
|
| 18 |
+
--text: #0f172a;
|
| 19 |
+
--text-2: #334155;
|
| 20 |
+
--muted: #64748b;
|
| 21 |
+
--placeholder: #94a3b8;
|
| 22 |
+
|
| 23 |
+
/* Primary — Blue-600 */
|
| 24 |
+
--primary: #2563eb;
|
| 25 |
+
--primary-h: #1d4ed8;
|
| 26 |
+
--primary-50: #eff6ff;
|
| 27 |
+
--primary-100: #dbeafe;
|
| 28 |
+
--primary-text: #1e40af;
|
| 29 |
+
|
| 30 |
+
/* Success — Green-600 */
|
| 31 |
+
--success: #16a34a;
|
| 32 |
+
--success-h: #15803d;
|
| 33 |
+
--success-50: #f0fdf4;
|
| 34 |
+
--success-100: #dcfce7;
|
| 35 |
+
--success-border: #86efac;
|
| 36 |
+
|
| 37 |
+
/* Warning — Amber-600 */
|
| 38 |
+
--warn: #d97706;
|
| 39 |
+
--warn-h: #b45309;
|
| 40 |
+
--warn-50: #fffbeb;
|
| 41 |
+
--warn-100: #fef3c7;
|
| 42 |
+
--warn-border: #fcd34d;
|
| 43 |
+
|
| 44 |
+
/* Danger — Red-600 */
|
| 45 |
+
--danger: #dc2626;
|
| 46 |
+
--danger-h: #b91c1c;
|
| 47 |
+
--danger-50: #fef2f2;
|
| 48 |
+
--danger-100: #fee2e2;
|
| 49 |
+
--danger-border: #fca5a5;
|
| 50 |
+
|
| 51 |
+
/* Info — Sky-600 */
|
| 52 |
+
--info: #0284c7;
|
| 53 |
+
--info-50: #f0f9ff;
|
| 54 |
+
--info-100: #e0f2fe;
|
| 55 |
+
--info-border: #7dd3fc;
|
| 56 |
+
|
| 57 |
+
/* Layout */
|
| 58 |
+
--r: 8px;
|
| 59 |
+
--r-sm: 5px;
|
| 60 |
+
--r-lg: 12px;
|
| 61 |
+
--shadow-sm: 0 1px 2px rgba(0,0,0,.05);
|
| 62 |
+
--shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.05);
|
| 63 |
+
--shadow-md: 0 4px 6px rgba(0,0,0,.06), 0 2px 4px rgba(0,0,0,.05);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/* ── Base ─────────────────────────────────────────── */
|
| 67 |
+
body {
|
| 68 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 69 |
+
background: var(--bg);
|
| 70 |
+
color: var(--text);
|
| 71 |
+
line-height: 1.6;
|
| 72 |
+
font-size: 14px;
|
| 73 |
+
-webkit-font-smoothing: antialiased;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* ── Topbar ───────────────────────────────────────── */
|
| 77 |
+
.topbar {
|
| 78 |
+
background: var(--surface);
|
| 79 |
+
border-bottom: 1px solid var(--border);
|
| 80 |
+
height: 56px;
|
| 81 |
+
padding: 0 28px;
|
| 82 |
+
display: flex;
|
| 83 |
+
align-items: center;
|
| 84 |
+
justify-content: space-between;
|
| 85 |
+
position: sticky;
|
| 86 |
+
top: 0;
|
| 87 |
+
z-index: 100;
|
| 88 |
+
box-shadow: var(--shadow-sm);
|
| 89 |
+
}
|
| 90 |
+
.topbar-logo {
|
| 91 |
+
font-size: 18px;
|
| 92 |
+
font-weight: 800;
|
| 93 |
+
color: var(--primary);
|
| 94 |
+
letter-spacing: -.3px;
|
| 95 |
+
display: flex;
|
| 96 |
+
align-items: center;
|
| 97 |
+
gap: 7px;
|
| 98 |
+
}
|
| 99 |
+
.topbar-logo em { color: var(--text); font-style: normal; font-weight: 600; }
|
| 100 |
+
.topbar-status {
|
| 101 |
+
display: flex;
|
| 102 |
+
align-items: center;
|
| 103 |
+
gap: 8px;
|
| 104 |
+
font-size: 12px;
|
| 105 |
+
color: var(--muted);
|
| 106 |
+
}
|
| 107 |
+
.status-dot {
|
| 108 |
+
width: 8px; height: 8px;
|
| 109 |
+
border-radius: 50%;
|
| 110 |
+
background: var(--danger);
|
| 111 |
+
flex-shrink: 0;
|
| 112 |
+
transition: background .3s;
|
| 113 |
+
}
|
| 114 |
+
.status-dot.online { background: var(--success); }
|
| 115 |
+
.status-dot.checking { background: var(--warn); animation: pulse 1s ease-in-out infinite; }
|
| 116 |
+
|
| 117 |
+
/* ── Container ────────────────────────────────────── */
|
| 118 |
+
.container { max-width: 1280px; margin: 0 auto; padding: 28px 28px 60px; }
|
| 119 |
+
|
| 120 |
+
/* ── Hero ─────────────────────────────────────────── */
|
| 121 |
+
.hero {
|
| 122 |
+
padding-bottom: 20px;
|
| 123 |
+
margin-bottom: 24px;
|
| 124 |
+
border-bottom: 1px solid var(--border);
|
| 125 |
+
}
|
| 126 |
+
.hero h1 {
|
| 127 |
+
font-size: 24px;
|
| 128 |
+
font-weight: 800;
|
| 129 |
+
color: var(--text);
|
| 130 |
+
letter-spacing: -.4px;
|
| 131 |
+
margin-bottom: 4px;
|
| 132 |
+
}
|
| 133 |
+
.hero h1 span { color: var(--primary); }
|
| 134 |
+
.hero p { color: var(--muted); font-size: 13px; }
|
| 135 |
+
|
| 136 |
+
/* ── Token Bar ────────────────────────────────────── */
|
| 137 |
+
.token-bar {
|
| 138 |
+
background: var(--surface);
|
| 139 |
+
border: 1px solid var(--border);
|
| 140 |
+
border-radius: var(--r);
|
| 141 |
+
padding: 10px 16px;
|
| 142 |
+
margin-bottom: 20px;
|
| 143 |
+
display: flex;
|
| 144 |
+
align-items: center;
|
| 145 |
+
gap: 10px;
|
| 146 |
+
box-shadow: var(--shadow-sm);
|
| 147 |
+
}
|
| 148 |
+
.token-label {
|
| 149 |
+
font-size: 10px;
|
| 150 |
+
font-weight: 700;
|
| 151 |
+
text-transform: uppercase;
|
| 152 |
+
letter-spacing: .8px;
|
| 153 |
+
color: var(--muted);
|
| 154 |
+
white-space: nowrap;
|
| 155 |
+
}
|
| 156 |
+
.token-val {
|
| 157 |
+
flex: 1;
|
| 158 |
+
font-family: 'Consolas', 'Cascadia Code', monospace;
|
| 159 |
+
font-size: 12px;
|
| 160 |
+
color: var(--primary-text);
|
| 161 |
+
overflow: hidden;
|
| 162 |
+
text-overflow: ellipsis;
|
| 163 |
+
white-space: nowrap;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/* ── Main Tabs ────────────────────────────────────── */
|
| 167 |
+
.main-tabs {
|
| 168 |
+
display: flex;
|
| 169 |
+
gap: 0;
|
| 170 |
+
border-bottom: 2px solid var(--border);
|
| 171 |
+
margin-bottom: 24px;
|
| 172 |
+
overflow-x: auto;
|
| 173 |
+
}
|
| 174 |
+
.main-tabs button {
|
| 175 |
+
background: none;
|
| 176 |
+
border: none;
|
| 177 |
+
border-bottom: 2px solid transparent;
|
| 178 |
+
margin-bottom: -2px;
|
| 179 |
+
color: var(--muted);
|
| 180 |
+
padding: 10px 20px;
|
| 181 |
+
font-size: 13px;
|
| 182 |
+
font-weight: 600;
|
| 183 |
+
cursor: pointer;
|
| 184 |
+
white-space: nowrap;
|
| 185 |
+
transition: color .15s, border-color .15s;
|
| 186 |
+
font-family: inherit;
|
| 187 |
+
}
|
| 188 |
+
.main-tabs button.active { color: var(--primary); border-bottom-color: var(--primary); }
|
| 189 |
+
.main-tabs button:hover:not(.active) { color: var(--text-2); }
|
| 190 |
+
|
| 191 |
+
/* ── Cards ────────────────────────────────────────── */
|
| 192 |
+
.card {
|
| 193 |
+
background: var(--surface);
|
| 194 |
+
border: 1px solid var(--border);
|
| 195 |
+
border-radius: var(--r);
|
| 196 |
+
padding: 20px 22px;
|
| 197 |
+
margin-bottom: 16px;
|
| 198 |
+
box-shadow: var(--shadow);
|
| 199 |
+
}
|
| 200 |
+
.card-header {
|
| 201 |
+
display: flex;
|
| 202 |
+
align-items: center;
|
| 203 |
+
gap: 8px;
|
| 204 |
+
font-size: 11px;
|
| 205 |
+
font-weight: 700;
|
| 206 |
+
text-transform: uppercase;
|
| 207 |
+
letter-spacing: .7px;
|
| 208 |
+
color: var(--muted);
|
| 209 |
+
padding-bottom: 12px;
|
| 210 |
+
margin-bottom: 14px;
|
| 211 |
+
border-bottom: 1px solid var(--border);
|
| 212 |
+
}
|
| 213 |
+
.card-header-row {
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
justify-content: space-between;
|
| 217 |
+
padding-bottom: 12px;
|
| 218 |
+
margin-bottom: 14px;
|
| 219 |
+
border-bottom: 1px solid var(--border);
|
| 220 |
+
}
|
| 221 |
+
.card-header-row .card-title {
|
| 222 |
+
font-size: 11px;
|
| 223 |
+
font-weight: 700;
|
| 224 |
+
text-transform: uppercase;
|
| 225 |
+
letter-spacing: .7px;
|
| 226 |
+
color: var(--muted);
|
| 227 |
+
display: flex;
|
| 228 |
+
align-items: center;
|
| 229 |
+
gap: 8px;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
/* ── Buttons ──────────────────────────────────────── */
|
| 233 |
+
button, .btn {
|
| 234 |
+
display: inline-flex;
|
| 235 |
+
align-items: center;
|
| 236 |
+
justify-content: center;
|
| 237 |
+
gap: 5px;
|
| 238 |
+
padding: 8px 16px;
|
| 239 |
+
border: 1px solid transparent;
|
| 240 |
+
border-radius: var(--r-sm);
|
| 241 |
+
font-size: 13px;
|
| 242 |
+
font-weight: 600;
|
| 243 |
+
cursor: pointer;
|
| 244 |
+
transition: background .12s, filter .1s, transform .08s, box-shadow .12s;
|
| 245 |
+
white-space: nowrap;
|
| 246 |
+
font-family: inherit;
|
| 247 |
+
text-align: center;
|
| 248 |
+
}
|
| 249 |
+
button:active:not(:disabled) { transform: scale(.97); }
|
| 250 |
+
button:disabled { opacity: .45; cursor: not-allowed; }
|
| 251 |
+
|
| 252 |
+
.btn-primary { background: var(--primary); color: #fff; border-color: var(--primary-h); }
|
| 253 |
+
.btn-primary:hover:not(:disabled) { background: var(--primary-h); }
|
| 254 |
+
.btn-success { background: var(--success); color: #fff; border-color: var(--success-h); }
|
| 255 |
+
.btn-success:hover:not(:disabled) { background: var(--success-h); }
|
| 256 |
+
.btn-warn { background: var(--warn); color: #fff; border-color: var(--warn-h); }
|
| 257 |
+
.btn-warn:hover:not(:disabled) { background: var(--warn-h); }
|
| 258 |
+
.btn-danger { background: var(--danger); color: #fff; border-color: var(--danger-h); }
|
| 259 |
+
.btn-danger:hover:not(:disabled) { background: var(--danger-h); }
|
| 260 |
+
.btn-ghost {
|
| 261 |
+
background: var(--surface);
|
| 262 |
+
color: var(--text-2);
|
| 263 |
+
border-color: var(--border);
|
| 264 |
+
box-shadow: var(--shadow-sm);
|
| 265 |
+
}
|
| 266 |
+
.btn-ghost:hover:not(:disabled) { background: var(--bg); border-color: var(--border-2); }
|
| 267 |
+
|
| 268 |
+
.btn-sm { padding: 5px 11px; font-size: 12px; }
|
| 269 |
+
.btn-full { width: 100%; }
|
| 270 |
+
|
| 271 |
+
/* ── Forms ────────────────────────────────────────── */
|
| 272 |
+
.form-group { margin-bottom: 13px; }
|
| 273 |
+
.form-group label {
|
| 274 |
+
display: block;
|
| 275 |
+
font-size: 11px;
|
| 276 |
+
font-weight: 600;
|
| 277 |
+
color: var(--muted);
|
| 278 |
+
margin-bottom: 5px;
|
| 279 |
+
text-transform: uppercase;
|
| 280 |
+
letter-spacing: .5px;
|
| 281 |
+
}
|
| 282 |
+
input, select, .form-input {
|
| 283 |
+
width: 100%;
|
| 284 |
+
background: var(--surface);
|
| 285 |
+
border: 1px solid var(--border);
|
| 286 |
+
border-radius: var(--r-sm);
|
| 287 |
+
color: var(--text);
|
| 288 |
+
padding: 8px 11px;
|
| 289 |
+
font-size: 13px;
|
| 290 |
+
font-family: inherit;
|
| 291 |
+
transition: border-color .15s, box-shadow .15s;
|
| 292 |
+
}
|
| 293 |
+
input:focus, select:focus, .form-input:focus {
|
| 294 |
+
outline: none;
|
| 295 |
+
border-color: var(--primary);
|
| 296 |
+
box-shadow: 0 0 0 3px var(--primary-100);
|
| 297 |
+
}
|
| 298 |
+
input::placeholder { color: var(--placeholder); }
|
| 299 |
+
input[readonly] { background: var(--surface-2); color: var(--muted); cursor: default; }
|
| 300 |
+
|
| 301 |
+
/* ── Response Box ─────────────────────────────────── */
|
| 302 |
+
.resp-box {
|
| 303 |
+
margin-top: 10px;
|
| 304 |
+
padding: 12px 14px;
|
| 305 |
+
border-radius: var(--r-sm);
|
| 306 |
+
font-size: 12px;
|
| 307 |
+
font-family: 'Consolas', 'Cascadia Code', monospace;
|
| 308 |
+
background: var(--surface-2);
|
| 309 |
+
border: 1px solid var(--border);
|
| 310 |
+
white-space: pre-wrap;
|
| 311 |
+
word-break: break-all;
|
| 312 |
+
max-height: 280px;
|
| 313 |
+
overflow-y: auto;
|
| 314 |
+
line-height: 1.6;
|
| 315 |
+
color: var(--text-2);
|
| 316 |
+
}
|
| 317 |
+
.resp-box.ok { border-color: var(--success-border); color: var(--success); background: var(--success-50); }
|
| 318 |
+
.resp-box.err { border-color: var(--danger-border); color: var(--danger); background: var(--danger-50); }
|
| 319 |
+
|
| 320 |
+
/* ── Callouts ─────────────────────────────────────── */
|
| 321 |
+
.callout {
|
| 322 |
+
padding: 12px 15px;
|
| 323 |
+
border-radius: var(--r-sm);
|
| 324 |
+
font-size: 13px;
|
| 325 |
+
line-height: 1.65;
|
| 326 |
+
margin-bottom: 14px;
|
| 327 |
+
border-left: 3px solid;
|
| 328 |
+
}
|
| 329 |
+
.callout-info { background: var(--info-50); border-color: var(--info); color: #0c4a6e; }
|
| 330 |
+
.callout-success { background: var(--success-50); border-color: var(--success); color: #14532d; }
|
| 331 |
+
.callout-warn { background: var(--warn-50); border-color: var(--warn); color: #78350f; }
|
| 332 |
+
.callout-danger { background: var(--danger-50); border-color: var(--danger); color: #7f1d1d; }
|
| 333 |
+
|
| 334 |
+
/* ── Badges & Tags ────────────────────────────────── */
|
| 335 |
+
.badge {
|
| 336 |
+
display: inline-flex;
|
| 337 |
+
align-items: center;
|
| 338 |
+
padding: 2px 9px;
|
| 339 |
+
border-radius: 4px;
|
| 340 |
+
font-size: 11px;
|
| 341 |
+
font-weight: 700;
|
| 342 |
+
letter-spacing: .4px;
|
| 343 |
+
text-transform: uppercase;
|
| 344 |
+
}
|
| 345 |
+
.badge-success { background: var(--success-100); color: var(--success); border: 1px solid var(--success-border); }
|
| 346 |
+
.badge-warn { background: var(--warn-100); color: var(--warn); border: 1px solid var(--warn-border); }
|
| 347 |
+
.badge-danger { background: var(--danger-100); color: var(--danger); border: 1px solid var(--danger-border); }
|
| 348 |
+
.badge-info { background: var(--info-100); color: var(--info); border: 1px solid var(--info-border); }
|
| 349 |
+
.badge-muted { background: var(--surface-2); color: var(--muted); border: 1px solid var(--border); }
|
| 350 |
+
|
| 351 |
+
.tag {
|
| 352 |
+
display: inline-block;
|
| 353 |
+
padding: 1px 7px;
|
| 354 |
+
border-radius: 4px;
|
| 355 |
+
font-size: 11px;
|
| 356 |
+
background: var(--primary-50);
|
| 357 |
+
color: var(--primary-text);
|
| 358 |
+
margin: 2px;
|
| 359 |
+
font-weight: 600;
|
| 360 |
+
border: 1px solid var(--primary-100);
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
/* ── Pill ─────────────────────────────────────────── */
|
| 364 |
+
.pill {
|
| 365 |
+
display: inline-flex;
|
| 366 |
+
align-items: center;
|
| 367 |
+
gap: 4px;
|
| 368 |
+
padding: 3px 9px;
|
| 369 |
+
border-radius: 4px;
|
| 370 |
+
font-size: 11px;
|
| 371 |
+
font-weight: 700;
|
| 372 |
+
text-transform: uppercase;
|
| 373 |
+
letter-spacing: .3px;
|
| 374 |
+
}
|
| 375 |
+
.pill-ok { background: var(--success-100); color: var(--success); border: 1px solid var(--success-border); }
|
| 376 |
+
.pill-err { background: var(--danger-100); color: var(--danger); border: 1px solid var(--danger-border); }
|
| 377 |
+
.pill-warn{ background: var(--warn-100); color: var(--warn); border: 1px solid var(--warn-border); }
|
| 378 |
+
|
| 379 |
+
/* ── Step Bar ─────────────────────────────────────── */
|
| 380 |
+
.step-bar {
|
| 381 |
+
display: flex;
|
| 382 |
+
gap: 8px;
|
| 383 |
+
margin-bottom: 20px;
|
| 384 |
+
overflow-x: auto;
|
| 385 |
+
}
|
| 386 |
+
.step-item {
|
| 387 |
+
flex: 1;
|
| 388 |
+
min-width: 120px;
|
| 389 |
+
background: var(--surface);
|
| 390 |
+
border: 1px solid var(--border);
|
| 391 |
+
border-radius: var(--r);
|
| 392 |
+
padding: 11px 14px;
|
| 393 |
+
text-align: center;
|
| 394 |
+
box-shadow: var(--shadow-sm);
|
| 395 |
+
transition: border-color .2s;
|
| 396 |
+
}
|
| 397 |
+
.step-item.active { border-color: var(--primary); background: var(--primary-50); }
|
| 398 |
+
.step-item.done { border-color: var(--success-border); background: var(--success-50); }
|
| 399 |
+
.step-num {
|
| 400 |
+
width: 26px; height: 26px;
|
| 401 |
+
border-radius: 50%;
|
| 402 |
+
background: var(--border);
|
| 403 |
+
color: var(--muted);
|
| 404 |
+
display: inline-flex;
|
| 405 |
+
align-items: center;
|
| 406 |
+
justify-content: center;
|
| 407 |
+
font-size: 12px;
|
| 408 |
+
font-weight: 700;
|
| 409 |
+
margin-bottom: 5px;
|
| 410 |
+
}
|
| 411 |
+
.step-item.active .step-num { background: var(--primary); color: #fff; }
|
| 412 |
+
.step-item.done .step-num { background: var(--success); color: #fff; font-size: 14px; }
|
| 413 |
+
.step-label { font-size: 12px; font-weight: 700; color: var(--text-2); }
|
| 414 |
+
.step-sub { font-size: 11px; color: var(--muted); margin-top: 2px; }
|
| 415 |
+
|
| 416 |
+
/* ── Compare Grid ─────────────────────────────────── */
|
| 417 |
+
.compare-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px; }
|
| 418 |
+
.compare-side {
|
| 419 |
+
background: var(--surface);
|
| 420 |
+
border-radius: var(--r);
|
| 421 |
+
padding: 16px;
|
| 422 |
+
border: 1px solid var(--border);
|
| 423 |
+
box-shadow: var(--shadow-sm);
|
| 424 |
+
}
|
| 425 |
+
.compare-side.success { border-top: 3px solid var(--success); }
|
| 426 |
+
.compare-side.danger { border-top: 3px solid var(--danger); }
|
| 427 |
+
.compare-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; margin-bottom: 10px; }
|
| 428 |
+
.compare-title.success { color: var(--success); }
|
| 429 |
+
.compare-title.danger { color: var(--danger); }
|
| 430 |
+
.context-list { display: flex; flex-direction: column; gap: 5px; font-size: 13px; margin-bottom: 10px; }
|
| 431 |
+
.context-list > div { color: var(--text-2); }
|
| 432 |
+
|
| 433 |
+
/* ── Risk Gauge ───────────────────────────────────── */
|
| 434 |
+
.gauge-wrap { text-align: center; padding: 10px 0 4px; }
|
| 435 |
+
.gauge-val {
|
| 436 |
+
font-size: 32px;
|
| 437 |
+
font-weight: 800;
|
| 438 |
+
letter-spacing: -1px;
|
| 439 |
+
line-height: 1;
|
| 440 |
+
margin-top: 2px;
|
| 441 |
+
}
|
| 442 |
+
.gauge-label {
|
| 443 |
+
font-size: 11px;
|
| 444 |
+
font-weight: 700;
|
| 445 |
+
text-transform: uppercase;
|
| 446 |
+
letter-spacing: .8px;
|
| 447 |
+
color: var(--muted);
|
| 448 |
+
margin-top: 4px;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
/* ── Level Bar ────────────────────────────────────── */
|
| 452 |
+
.level-bar { display: flex; gap: 4px; margin-top: 8px; }
|
| 453 |
+
.level-seg {
|
| 454 |
+
flex: 1; height: 7px; border-radius: 4px;
|
| 455 |
+
background: var(--border);
|
| 456 |
+
transition: background .4s;
|
| 457 |
+
}
|
| 458 |
+
.level-seg.seg-0 { background: var(--success); }
|
| 459 |
+
.level-seg.seg-1 { background: #84cc16; }
|
| 460 |
+
.level-seg.seg-2 { background: var(--warn); }
|
| 461 |
+
.level-seg.seg-3 { background: #f97316; }
|
| 462 |
+
.level-seg.seg-4 { background: var(--danger); }
|
| 463 |
+
|
| 464 |
+
/* ── Factor Bars ──────────────────────────────────── */
|
| 465 |
+
.factor-row { margin-bottom: 8px; }
|
| 466 |
+
.factor-label {
|
| 467 |
+
font-size: 12px;
|
| 468 |
+
color: var(--muted);
|
| 469 |
+
margin-bottom: 4px;
|
| 470 |
+
display: flex;
|
| 471 |
+
justify-content: space-between;
|
| 472 |
+
}
|
| 473 |
+
.factor-bar-wrap { height: 8px; background: var(--border); border-radius: 4px; overflow: hidden; }
|
| 474 |
+
.factor-bar { height: 100%; border-radius: 4px; transition: width .7s ease; }
|
| 475 |
+
|
| 476 |
+
/* ── Decision Panel ───────────────────────────────── */
|
| 477 |
+
.decision-panel {
|
| 478 |
+
border-radius: var(--r-sm);
|
| 479 |
+
overflow: hidden;
|
| 480 |
+
border: 1px solid var(--border);
|
| 481 |
+
margin-top: 8px;
|
| 482 |
+
}
|
| 483 |
+
.decision-header {
|
| 484 |
+
padding: 11px 14px;
|
| 485 |
+
font-size: 14px;
|
| 486 |
+
font-weight: 800;
|
| 487 |
+
text-align: center;
|
| 488 |
+
letter-spacing: .3px;
|
| 489 |
+
}
|
| 490 |
+
.decision-header.success { background: var(--success-50); color: var(--success); border-bottom: 1px solid var(--success-border); }
|
| 491 |
+
.decision-header.challenge{ background: var(--warn-50); color: var(--warn); border-bottom: 1px solid var(--warn-border); }
|
| 492 |
+
.decision-header.blocked { background: var(--danger-50); color: var(--danger); border-bottom: 1px solid var(--danger-border); }
|
| 493 |
+
.decision-body { padding: 12px 14px; font-size: 13px; background: var(--surface); color: var(--text-2); }
|
| 494 |
+
|
| 495 |
+
/* ── Attack Log ───────────────────────────────────── */
|
| 496 |
+
.attack-log {
|
| 497 |
+
background: #1e1b4b;
|
| 498 |
+
border-radius: var(--r-sm);
|
| 499 |
+
padding: 10px 13px;
|
| 500 |
+
font-family: 'Consolas', monospace;
|
| 501 |
+
font-size: 12px;
|
| 502 |
+
height: 190px;
|
| 503 |
+
overflow-y: auto;
|
| 504 |
+
margin-top: 12px;
|
| 505 |
+
}
|
| 506 |
+
.attack-line { margin: 2px 0; color: #c7d2fe; }
|
| 507 |
+
.attack-detected { color: #fcd34d; font-weight: 700; }
|
| 508 |
+
.attack-blocked { color: #f9a8d4; font-weight: 700; }
|
| 509 |
+
|
| 510 |
+
/* ── Anomaly Feed ─────────────────────────────────── */
|
| 511 |
+
.anomaly-item {
|
| 512 |
+
background: var(--danger-50);
|
| 513 |
+
border: 1px solid var(--danger-border);
|
| 514 |
+
border-radius: var(--r-sm);
|
| 515 |
+
padding: 10px 13px;
|
| 516 |
+
margin-bottom: 8px;
|
| 517 |
+
display: flex;
|
| 518 |
+
align-items: flex-start;
|
| 519 |
+
gap: 10px;
|
| 520 |
+
animation: slideIn .25s ease;
|
| 521 |
+
}
|
| 522 |
+
.anomaly-type { font-weight: 700; font-size: 13px; color: var(--danger); }
|
| 523 |
+
.anomaly-meta { font-size: 11px; color: var(--muted); margin-top: 3px; }
|
| 524 |
+
|
| 525 |
+
/* ── Monitor Badge ────────────────────────────────── */
|
| 526 |
+
.monitor-badge {
|
| 527 |
+
display: inline-flex;
|
| 528 |
+
align-items: center;
|
| 529 |
+
gap: 5px;
|
| 530 |
+
padding: 3px 10px;
|
| 531 |
+
border-radius: 4px;
|
| 532 |
+
font-size: 11px;
|
| 533 |
+
font-weight: 700;
|
| 534 |
+
text-transform: uppercase;
|
| 535 |
+
letter-spacing: .4px;
|
| 536 |
+
transition: all .2s;
|
| 537 |
+
}
|
| 538 |
+
.monitor-badge.mon-on { background: var(--danger-100); color: var(--danger); border: 1px solid var(--danger-border); animation: pulse 1.4s ease-in-out infinite; }
|
| 539 |
+
.monitor-badge.mon-off { background: var(--surface-2); color: var(--muted); border: 1px solid var(--border); }
|
| 540 |
+
|
| 541 |
+
/* ── Monitor Stats ────────────────────────────────── */
|
| 542 |
+
.monitor-stats {
|
| 543 |
+
display: flex;
|
| 544 |
+
flex-wrap: wrap;
|
| 545 |
+
gap: 8px;
|
| 546 |
+
padding: 10px 14px;
|
| 547 |
+
background: var(--surface-2);
|
| 548 |
+
border: 1px solid var(--border);
|
| 549 |
+
border-radius: var(--r-sm);
|
| 550 |
+
font-size: 12px;
|
| 551 |
+
margin-bottom: 10px;
|
| 552 |
+
}
|
| 553 |
+
.ms-item { display: flex; flex-direction: column; align-items: center; min-width: 56px; }
|
| 554 |
+
.ms-val { font-size: 20px; font-weight: 800; line-height: 1; letter-spacing: -.3px; }
|
| 555 |
+
.ms-lbl { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .4px; margin-top: 2px; }
|
| 556 |
+
|
| 557 |
+
/* ── Monitor Log ──────────────────────────────────── */
|
| 558 |
+
.monitor-log {
|
| 559 |
+
background: #0f172a;
|
| 560 |
+
border-radius: var(--r-sm);
|
| 561 |
+
padding: 10px 13px;
|
| 562 |
+
font-family: 'Consolas', monospace;
|
| 563 |
+
font-size: 11.5px;
|
| 564 |
+
height: 140px;
|
| 565 |
+
overflow-y: auto;
|
| 566 |
+
line-height: 1.7;
|
| 567 |
+
margin-top: 10px;
|
| 568 |
+
}
|
| 569 |
+
.ml-ok { color: #86efac; }
|
| 570 |
+
.ml-threat { color: #fca5a5; font-weight: 700; }
|
| 571 |
+
.ml-warn { color: #fcd34d; }
|
| 572 |
+
.ml-info { color: #93c5fd; }
|
| 573 |
+
|
| 574 |
+
/* ── Stat Boxes ───────────────────────────────────── */
|
| 575 |
+
.stat-box {
|
| 576 |
+
background: var(--surface);
|
| 577 |
+
border: 1px solid var(--border);
|
| 578 |
+
border-radius: var(--r);
|
| 579 |
+
padding: 18px 12px;
|
| 580 |
+
text-align: center;
|
| 581 |
+
box-shadow: var(--shadow-sm);
|
| 582 |
+
}
|
| 583 |
+
.stat-num { font-size: 32px; font-weight: 800; line-height: 1; letter-spacing: -.5px; }
|
| 584 |
+
.stat-label { font-size: 10px; color: var(--muted); margin-top: 6px; text-transform: uppercase; letter-spacing: .6px; }
|
| 585 |
+
|
| 586 |
+
/* ── Trust Gauge ──────────────────────────────────���─ */
|
| 587 |
+
.trust-label { font-size: 11px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; margin-top: 4px; text-align: center; }
|
| 588 |
+
|
| 589 |
+
/* ── Behavior Bars ────────────────────────────────── */
|
| 590 |
+
.behavior-bar-group { margin-bottom: 10px; }
|
| 591 |
+
.behavior-bar-header {
|
| 592 |
+
display: flex;
|
| 593 |
+
justify-content: space-between;
|
| 594 |
+
font-size: 12px;
|
| 595 |
+
margin-bottom: 4px;
|
| 596 |
+
color: var(--text-2);
|
| 597 |
+
}
|
| 598 |
+
.behavior-bar-track { height: 8px; background: var(--border); border-radius: 4px; overflow: hidden; }
|
| 599 |
+
.behavior-bar-fill { height: 100%; border-radius: 4px; transition: width .5s, background .5s; }
|
| 600 |
+
|
| 601 |
+
/* ── Code inline ──────────────────────────────────── */
|
| 602 |
+
code {
|
| 603 |
+
background: var(--primary-50);
|
| 604 |
+
border: 1px solid var(--primary-100);
|
| 605 |
+
border-radius: 3px;
|
| 606 |
+
padding: 1px 5px;
|
| 607 |
+
font-size: 11.5px;
|
| 608 |
+
color: var(--primary-text);
|
| 609 |
+
font-family: 'Consolas', monospace;
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
/* ── QR Code ──────────────────────────────────────── */
|
| 613 |
+
.qr-img {
|
| 614 |
+
max-width: 180px;
|
| 615 |
+
border: 3px solid var(--border);
|
| 616 |
+
border-radius: var(--r);
|
| 617 |
+
display: block;
|
| 618 |
+
margin: 10px auto;
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
/* ── Compare (side-by-side) ───────────────────────── */
|
| 622 |
+
.side-compare { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 10px; }
|
| 623 |
+
.side-item { border: 1px solid var(--border); border-radius: var(--r-sm); padding: 14px; background: var(--surface); }
|
| 624 |
+
.side-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; margin-bottom: 8px; }
|
| 625 |
+
|
| 626 |
+
/* ── Email Status ─────────────────────────────────── */
|
| 627 |
+
.env-row {
|
| 628 |
+
display: flex;
|
| 629 |
+
justify-content: space-between;
|
| 630 |
+
align-items: center;
|
| 631 |
+
padding: 5px 0;
|
| 632 |
+
border-bottom: 1px solid var(--border);
|
| 633 |
+
font-size: 12px;
|
| 634 |
+
}
|
| 635 |
+
.env-key { font-family: 'Consolas', monospace; color: var(--text-2); }
|
| 636 |
+
|
| 637 |
+
/* ── Grids ────────────────────────────────────────── */
|
| 638 |
+
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
| 639 |
+
.grid-3 { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; }
|
| 640 |
+
.grid-4 { display: grid; grid-template-columns: repeat(4,1fr); gap: 12px; }
|
| 641 |
+
|
| 642 |
+
/* ── Utilities ────────────────────────────────────── */
|
| 643 |
+
.flex { display: flex; }
|
| 644 |
+
.flex-wrap { flex-wrap: wrap; }
|
| 645 |
+
.flex-col { flex-direction: column; }
|
| 646 |
+
.items-center { align-items: center; }
|
| 647 |
+
.items-start { align-items: flex-start; }
|
| 648 |
+
.justify-between{ justify-content: space-between; }
|
| 649 |
+
.justify-center{ justify-content: center; }
|
| 650 |
+
.gap-1 { gap: 4px; }
|
| 651 |
+
.gap-2 { gap: 8px; }
|
| 652 |
+
.gap-3 { gap: 12px; }
|
| 653 |
+
.gap-4 { gap: 16px; }
|
| 654 |
+
.flex-1 { flex: 1; }
|
| 655 |
+
.min-w-0{ min-width: 0; }
|
| 656 |
+
.mt-1 { margin-top: 4px; }
|
| 657 |
+
.mt-2 { margin-top: 8px; }
|
| 658 |
+
.mt-3 { margin-top: 12px; }
|
| 659 |
+
.mt-4 { margin-top: 16px; }
|
| 660 |
+
.mb-1 { margin-bottom: 4px; }
|
| 661 |
+
.mb-2 { margin-bottom: 8px; }
|
| 662 |
+
.mb-3 { margin-bottom: 12px; }
|
| 663 |
+
.mb-4 { margin-bottom: 16px; }
|
| 664 |
+
.ml-auto{ margin-left: auto; }
|
| 665 |
+
.ml-4 { margin-left: 16px; }
|
| 666 |
+
.p-3 { padding: 12px; }
|
| 667 |
+
.p-4 { padding: 16px; }
|
| 668 |
+
.text-sm { font-size: 12px; }
|
| 669 |
+
.text-xs { font-size: 11px; }
|
| 670 |
+
.text-muted{ color: var(--muted); }
|
| 671 |
+
.text-2 { color: var(--text-2); }
|
| 672 |
+
.text-primary { color: var(--primary); }
|
| 673 |
+
.text-success { color: var(--success); }
|
| 674 |
+
.text-warn { color: var(--warn); }
|
| 675 |
+
.text-danger { color: var(--danger); }
|
| 676 |
+
.text-center { text-align: center; }
|
| 677 |
+
.font-bold { font-weight: 700; }
|
| 678 |
+
.font-600 { font-weight: 600; }
|
| 679 |
+
.font-800 { font-weight: 800; }
|
| 680 |
+
.w-full { width: 100%; }
|
| 681 |
+
.uppercase { text-transform: uppercase; }
|
| 682 |
+
.letter-wide { letter-spacing: .5px; }
|
| 683 |
+
.mono { font-family: 'Consolas', monospace; }
|
| 684 |
+
.border-t { border-top: 1px solid var(--border); padding-top: 12px; margin-top: 12px; }
|
| 685 |
+
|
| 686 |
+
/* ── Animations ───────────────────────────────────── */
|
| 687 |
+
@keyframes slideIn { from { opacity:0; transform:translateY(-6px); } to { opacity:1; transform:translateY(0); } }
|
| 688 |
+
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.35} }
|
| 689 |
+
@keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
|
| 690 |
+
|
| 691 |
+
/* ── Scrollbars ───────────────────────────────────── */
|
| 692 |
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 693 |
+
::-webkit-scrollbar-track { background: var(--surface-2); }
|
| 694 |
+
::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 3px; }
|
| 695 |
+
::-webkit-scrollbar-thumb:hover { background: var(--muted); }
|
| 696 |
+
|
| 697 |
+
/* ── Responsive ────────────────────────────��──────── */
|
| 698 |
+
@media (max-width: 900px) {
|
| 699 |
+
.grid-4, .grid-3 { grid-template-columns: 1fr 1fr; }
|
| 700 |
+
.compare-grid { grid-template-columns: 1fr; }
|
| 701 |
+
.side-compare { grid-template-columns: 1fr; }
|
| 702 |
+
}
|
| 703 |
+
@media (max-width: 600px) {
|
| 704 |
+
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
|
| 705 |
+
.container { padding: 16px; }
|
| 706 |
+
.topbar { padding: 0 16px; }
|
| 707 |
+
}
|
client/src/main.jsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import ReactDOM from 'react-dom/client'
|
| 3 |
+
import App from './App'
|
| 4 |
+
import './index.css'
|
| 5 |
+
|
| 6 |
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
| 7 |
+
<React.StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</React.StrictMode>
|
| 10 |
+
)
|
client/src/tabs/AdminTab.jsx
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import { req, saveToken, ENDPOINTS } from '../api';
|
| 3 |
+
import { Card, CardHeader, Callout, FormGroup, ResponseBox } from '../components/ui';
|
| 4 |
+
|
| 5 |
+
const { AUTH, ADMIN, RISK } = ENDPOINTS;
|
| 6 |
+
|
| 7 |
+
function EmailStatusBody({ data }) {
|
| 8 |
+
if (!data) return <p className="text-muted text-sm">Click Refresh to check email configuration.</p>;
|
| 9 |
+
const configured = data.configured;
|
| 10 |
+
const fields = data.fields || {};
|
| 11 |
+
return (
|
| 12 |
+
<div>
|
| 13 |
+
<div className="flex items-center gap-2 mb-3">
|
| 14 |
+
<span className={`pill ${configured ? 'pill-ok' : 'pill-err'}`}>
|
| 15 |
+
{configured ? '✔ Configured' : '✘ Not Configured'}
|
| 16 |
+
</span>
|
| 17 |
+
{data.mail_port && (
|
| 18 |
+
<span className="text-sm text-muted">
|
| 19 |
+
Port: {data.mail_port} · STARTTLS: {data.starttls ? 'Yes' : 'No'}
|
| 20 |
+
</span>
|
| 21 |
+
)}
|
| 22 |
+
</div>
|
| 23 |
+
{Object.entries(fields).map(([k, v]) => (
|
| 24 |
+
<div className="env-row" key={k}>
|
| 25 |
+
<span className="env-key">ADAPTIVEAUTH_{k}</span>
|
| 26 |
+
<span className={`pill ${v ? 'pill-ok' : 'pill-err'}`}>{v ? 'Set' : 'Missing'}</span>
|
| 27 |
+
</div>
|
| 28 |
+
))}
|
| 29 |
+
{!configured && data.setup_instructions && (
|
| 30 |
+
<Callout type="warn" style={{ marginTop: 12 }}>
|
| 31 |
+
<strong>Setup:</strong> Create a <code>.env</code> file with the missing fields above.<br />
|
| 32 |
+
<code style={{ background: 'none', border: 'none', padding: 0, color: 'inherit', display: 'block', marginTop: 4 }}>
|
| 33 |
+
{data.setup_instructions}
|
| 34 |
+
</code>
|
| 35 |
+
</Callout>
|
| 36 |
+
)}
|
| 37 |
+
</div>
|
| 38 |
+
);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export default function AdminTab({ onTokenSave }) {
|
| 42 |
+
const [adminLoginResp, setAdminLoginResp] = useState(null);
|
| 43 |
+
const [statsData, setStatsData] = useState(null);
|
| 44 |
+
const [emailData, setEmailData] = useState(null);
|
| 45 |
+
const [adminUserResp, setAdminUserResp] = useState(null);
|
| 46 |
+
const [adminRiskResp, setAdminRiskResp] = useState(null);
|
| 47 |
+
const [statsResp, setStatsResp] = useState(null);
|
| 48 |
+
const [adminUserId, setAdminUserId] = useState('');
|
| 49 |
+
const [loading, setLoading] = useState({});
|
| 50 |
+
|
| 51 |
+
const setLoad = (k, v) => setLoading(p => ({ ...p, [k]: v }));
|
| 52 |
+
|
| 53 |
+
const quickAdminLogin = async () => {
|
| 54 |
+
setLoad('login', true);
|
| 55 |
+
const r = await req(`${AUTH}/login`, 'POST', { email: 'demo.admin@adaptive.demo', password: 'Admin@Demo456!' }, false);
|
| 56 |
+
setAdminLoginResp(r);
|
| 57 |
+
if (r.ok && r.data?.access_token) { saveToken(r.data.access_token); onTokenSave?.(); }
|
| 58 |
+
setLoad('login', false);
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
+
const loadAdminStats = async () => {
|
| 62 |
+
setLoad('stats', true);
|
| 63 |
+
const r = await req(`${ADMIN}/statistics`);
|
| 64 |
+
if (r.ok) setStatsData(r.data);
|
| 65 |
+
setLoad('stats', false);
|
| 66 |
+
};
|
| 67 |
+
|
| 68 |
+
const checkEmailStatus = async () => {
|
| 69 |
+
setLoad('email', true);
|
| 70 |
+
const r = await req(`${ADMIN}/email-status`);
|
| 71 |
+
if (r.ok) setEmailData(r.data);
|
| 72 |
+
else setEmailData(null);
|
| 73 |
+
setLoad('email', false);
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
const userCall = async (url, method = 'GET') => {
|
| 77 |
+
setLoad('user', true);
|
| 78 |
+
setAdminUserResp(await req(url, method));
|
| 79 |
+
setLoad('user', false);
|
| 80 |
+
};
|
| 81 |
+
const riskCall = async (url) => {
|
| 82 |
+
setLoad('risk', true);
|
| 83 |
+
setAdminRiskResp(await req(url));
|
| 84 |
+
setLoad('risk', false);
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
const adminBlock = async () => {
|
| 88 |
+
if (!adminUserId) { alert('Enter user ID.'); return; }
|
| 89 |
+
await userCall(`${ADMIN}/users/${adminUserId}/block`, 'POST');
|
| 90 |
+
};
|
| 91 |
+
const adminUnblock = async () => {
|
| 92 |
+
if (!adminUserId) { alert('Enter user ID.'); return; }
|
| 93 |
+
await userCall(`${ADMIN}/users/${adminUserId}/unblock`, 'POST');
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
const STAT_ITEMS = [
|
| 97 |
+
{ key: 'total_users', label: 'Total Users', color: 'var(--info)' },
|
| 98 |
+
{ key: 'active_sessions', label: 'Active Sessions',color: 'var(--success)' },
|
| 99 |
+
{ key: 'high_risk_events_today',label: 'High Risk Today',color: 'var(--danger)' },
|
| 100 |
+
{ key: 'failed_logins_today', label: 'Failed Logins', color: 'var(--warn)' },
|
| 101 |
+
];
|
| 102 |
+
|
| 103 |
+
return (
|
| 104 |
+
<div>
|
| 105 |
+
<Callout type="warn">
|
| 106 |
+
Admin endpoints require an admin JWT token. Login with{' '}
|
| 107 |
+
<code>demo.admin@adaptive.demo</code> / <code>Admin@Demo456!</code> first.
|
| 108 |
+
</Callout>
|
| 109 |
+
|
| 110 |
+
{/* Quick Admin Login */}
|
| 111 |
+
<Card>
|
| 112 |
+
<CardHeader icon="🔑">Quick Admin Login</CardHeader>
|
| 113 |
+
<div className="flex items-center gap-3 flex-wrap">
|
| 114 |
+
<span className="text-sm text-2">
|
| 115 |
+
<code>demo.admin@adaptive.demo</code> / <code>Admin@Demo456!</code>
|
| 116 |
+
</span>
|
| 117 |
+
<button className="btn btn-primary btn-sm" onClick={quickAdminLogin} disabled={loading.login}>
|
| 118 |
+
{loading.login ? '…' : 'Login as Admin'}
|
| 119 |
+
</button>
|
| 120 |
+
</div>
|
| 121 |
+
<ResponseBox result={adminLoginResp} />
|
| 122 |
+
</Card>
|
| 123 |
+
|
| 124 |
+
{/* Stats */}
|
| 125 |
+
<div className="flex items-center gap-3 mb-3">
|
| 126 |
+
<button className="btn btn-ghost btn-sm" onClick={loadAdminStats} disabled={loading.stats}>
|
| 127 |
+
{loading.stats ? '…' : '🔄 Load Statistics'}
|
| 128 |
+
</button>
|
| 129 |
+
</div>
|
| 130 |
+
<div className="grid-4 mb-4">
|
| 131 |
+
{STAT_ITEMS.map(s => (
|
| 132 |
+
<div className="stat-box" key={s.key}>
|
| 133 |
+
<div className="stat-num" style={{ color: s.color }}>
|
| 134 |
+
{statsData ? (statsData[s.key] ?? '—') : '—'}
|
| 135 |
+
</div>
|
| 136 |
+
<div className="stat-label">{s.label}</div>
|
| 137 |
+
</div>
|
| 138 |
+
))}
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
{/* Email Status */}
|
| 142 |
+
<Card>
|
| 143 |
+
<CardHeader
|
| 144 |
+
icon="✉️"
|
| 145 |
+
actions={
|
| 146 |
+
<button className="btn btn-ghost btn-sm" onClick={checkEmailStatus} disabled={loading.email}>
|
| 147 |
+
{loading.email ? '…' : '🔄 Refresh'}
|
| 148 |
+
</button>
|
| 149 |
+
}
|
| 150 |
+
>
|
| 151 |
+
Email Service Status
|
| 152 |
+
</CardHeader>
|
| 153 |
+
<EmailStatusBody data={emailData} />
|
| 154 |
+
</Card>
|
| 155 |
+
|
| 156 |
+
<div className="grid-2">
|
| 157 |
+
{/* User Management */}
|
| 158 |
+
<Card>
|
| 159 |
+
<CardHeader icon="👥">User Management</CardHeader>
|
| 160 |
+
<div className="flex flex-wrap gap-2 mb-3">
|
| 161 |
+
<button className="btn btn-ghost btn-sm" onClick={() => userCall(`${ADMIN}/users`)}>List Users</button>
|
| 162 |
+
<button className="btn btn-ghost btn-sm" onClick={() => userCall(`${ADMIN}/sessions`)}>List Sessions</button>
|
| 163 |
+
</div>
|
| 164 |
+
<FormGroup label="User ID">
|
| 165 |
+
<input type="number" value={adminUserId} onChange={e => setAdminUserId(e.target.value)} placeholder="User ID" />
|
| 166 |
+
</FormGroup>
|
| 167 |
+
<div className="flex gap-2">
|
| 168 |
+
<button className="btn btn-success btn-sm flex-1" onClick={adminUnblock} disabled={loading.user}>Unblock</button>
|
| 169 |
+
<button className="btn btn-danger btn-sm flex-1" onClick={adminBlock} disabled={loading.user}>Block</button>
|
| 170 |
+
</div>
|
| 171 |
+
<ResponseBox result={adminUserResp} />
|
| 172 |
+
</Card>
|
| 173 |
+
|
| 174 |
+
{/* Risk Events */}
|
| 175 |
+
<Card>
|
| 176 |
+
<CardHeader icon="⚠️">Risk Events & Anomalies</CardHeader>
|
| 177 |
+
<div className="flex flex-wrap gap-2 mb-3">
|
| 178 |
+
<button className="btn btn-warn btn-sm" onClick={() => riskCall(`${ADMIN}/risk-events`)} disabled={loading.risk}>Risk Events</button>
|
| 179 |
+
<button className="btn btn-danger btn-sm" onClick={() => riskCall(`${ADMIN}/anomalies`)} disabled={loading.risk}>Active Anomalies</button>
|
| 180 |
+
<button className="btn btn-ghost btn-sm" onClick={() => riskCall(`${RISK}/overview`)} disabled={loading.risk}>Overview</button>
|
| 181 |
+
</div>
|
| 182 |
+
<ResponseBox result={adminRiskResp} />
|
| 183 |
+
</Card>
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
{/* Risk Stats */}
|
| 187 |
+
<Card>
|
| 188 |
+
<CardHeader icon="📈">Risk Statistics</CardHeader>
|
| 189 |
+
<div className="flex gap-2 mb-3">
|
| 190 |
+
{['day','week','month'].map(p => (
|
| 191 |
+
<button key={p} className="btn btn-ghost btn-sm" onClick={async () => {
|
| 192 |
+
setLoad('period', true);
|
| 193 |
+
setStatsResp(await req(`${ADMIN}/risk-statistics?period=${p}`));
|
| 194 |
+
setLoad('period', false);
|
| 195 |
+
}} disabled={loading.period}>
|
| 196 |
+
{p.charAt(0).toUpperCase() + p.slice(1)}
|
| 197 |
+
</button>
|
| 198 |
+
))}
|
| 199 |
+
</div>
|
| 200 |
+
<ResponseBox result={statsResp} />
|
| 201 |
+
</Card>
|
| 202 |
+
</div>
|
| 203 |
+
);
|
| 204 |
+
}
|
client/src/tabs/ApiTab.jsx
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import { req, saveToken, ENDPOINTS } from '../api';
|
| 3 |
+
import { Card, CardHeader, FormGroup, ResponseBox } from '../components/ui';
|
| 4 |
+
|
| 5 |
+
const { AUTH, USER, API: API_BASE } = ENDPOINTS;
|
| 6 |
+
|
| 7 |
+
export default function ApiTab({ onTokenSave }) {
|
| 8 |
+
// Register
|
| 9 |
+
const [regEmail, setRegEmail] = useState('');
|
| 10 |
+
const [regPassword, setRegPassword] = useState('');
|
| 11 |
+
const [regName, setRegName] = useState('');
|
| 12 |
+
const [regResp, setRegResp] = useState(null);
|
| 13 |
+
|
| 14 |
+
// Login
|
| 15 |
+
const [loginEmail, setLoginEmail] = useState('');
|
| 16 |
+
const [loginPassword, setLoginPassword] = useState('');
|
| 17 |
+
const [loginResp, setLoginResp] = useState(null);
|
| 18 |
+
|
| 19 |
+
// 2FA
|
| 20 |
+
const [qrCode, setQrCode] = useState('');
|
| 21 |
+
const [totpCode, setTotpCode] = useState('');
|
| 22 |
+
const [tfaResp, setTfaResp] = useState(null);
|
| 23 |
+
|
| 24 |
+
// Profile
|
| 25 |
+
const [profileResp, setProfileResp] = useState(null);
|
| 26 |
+
|
| 27 |
+
// Password
|
| 28 |
+
const [curPwd, setCurPwd] = useState('');
|
| 29 |
+
const [newPwd, setNewPwd] = useState('');
|
| 30 |
+
const [resetEmail, setResetEmail] = useState('');
|
| 31 |
+
const [pwdResp, setPwdResp] = useState(null);
|
| 32 |
+
|
| 33 |
+
// Protected
|
| 34 |
+
const [protResp, setProtResp] = useState(null);
|
| 35 |
+
|
| 36 |
+
// Loading
|
| 37 |
+
const [loading, setLoading] = useState({});
|
| 38 |
+
const setLoad = (k, v) => setLoading(p => ({ ...p, [k]: v }));
|
| 39 |
+
|
| 40 |
+
const doSave = (r, set, extract = false) => {
|
| 41 |
+
set(r);
|
| 42 |
+
if (extract && r.ok) {
|
| 43 |
+
const t = r.data?.access_token || r.data?.framework_decision?.access_token;
|
| 44 |
+
if (t) { saveToken(t); onTokenSave?.(); }
|
| 45 |
+
}
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
// Register
|
| 49 |
+
const apiRegister = async () => {
|
| 50 |
+
if (!regEmail || !regPassword) { alert('Email and password required.'); return; }
|
| 51 |
+
setLoad('reg', true);
|
| 52 |
+
const body = { email: regEmail, password: regPassword };
|
| 53 |
+
if (regName) body.full_name = regName;
|
| 54 |
+
doSave(await req(`${AUTH}/register`, 'POST', body, false), setRegResp);
|
| 55 |
+
setLoad('reg', false);
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
// Login
|
| 59 |
+
const apiLogin = async () => {
|
| 60 |
+
if (!loginEmail || !loginPassword) { alert('Email and password required.'); return; }
|
| 61 |
+
setLoad('login', true);
|
| 62 |
+
doSave(await req(`${AUTH}/login`, 'POST', { email: loginEmail, password: loginPassword }, false), setLoginResp, true);
|
| 63 |
+
setLoad('login', false);
|
| 64 |
+
};
|
| 65 |
+
const apiAdaptiveLogin = async () => {
|
| 66 |
+
if (!loginEmail || !loginPassword) { alert('Email and password required.'); return; }
|
| 67 |
+
setLoad('alogin', true);
|
| 68 |
+
doSave(await req(`${AUTH}/adaptive-login`, 'POST', { email: loginEmail, password: loginPassword }, false), setLoginResp, true);
|
| 69 |
+
setLoad('alogin', false);
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
// 2FA
|
| 73 |
+
const api2faEnable = async () => {
|
| 74 |
+
setLoad('2fa', true);
|
| 75 |
+
const r = await req(`${AUTH}/enable-2fa`, 'POST');
|
| 76 |
+
if (r.ok && r.data?.qr_code) setQrCode(r.data.qr_code);
|
| 77 |
+
setTfaResp(r);
|
| 78 |
+
setLoad('2fa', false);
|
| 79 |
+
};
|
| 80 |
+
const api2faVerify = async () => {
|
| 81 |
+
if (!totpCode) { alert('Enter TOTP code.'); return; }
|
| 82 |
+
setLoad('2fav', true);
|
| 83 |
+
setTfaResp(await req(`${AUTH}/verify-2fa`, 'POST', { otp: totpCode }));
|
| 84 |
+
setLoad('2fav', false);
|
| 85 |
+
};
|
| 86 |
+
const api2faDisable = async () => {
|
| 87 |
+
const pwd = prompt('Enter your password to disable 2FA:');
|
| 88 |
+
if (!pwd) return;
|
| 89 |
+
setLoad('2fad', true);
|
| 90 |
+
setTfaResp(await req(`${AUTH}/disable-2fa?password=${encodeURIComponent(pwd)}`, 'POST'));
|
| 91 |
+
setLoad('2fad', false);
|
| 92 |
+
};
|
| 93 |
+
const api2faStatus = async () => {
|
| 94 |
+
setLoad('2fas', true);
|
| 95 |
+
setTfaResp(await req(`${USER}/security`));
|
| 96 |
+
setLoad('2fas', false);
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
// Profile endpoints
|
| 100 |
+
const profileCall = async (url, label) => {
|
| 101 |
+
setLoad(label, true);
|
| 102 |
+
setProfileResp(await req(url));
|
| 103 |
+
setLoad(label, false);
|
| 104 |
+
};
|
| 105 |
+
const apiRevokeAll = async () => {
|
| 106 |
+
setLoad('revoke', true);
|
| 107 |
+
setProfileResp(await req(`${USER}/sessions/revoke`, 'POST', { session_ids: [], revoke_all: true }));
|
| 108 |
+
setLoad('revoke', false);
|
| 109 |
+
};
|
| 110 |
+
|
| 111 |
+
// Password
|
| 112 |
+
const apiChangePwd = async () => {
|
| 113 |
+
setLoad('chpwd', true);
|
| 114 |
+
setPwdResp(await req(`${USER}/change-password`, 'POST', { current_password: curPwd, new_password: newPwd, confirm_password: newPwd }));
|
| 115 |
+
setLoad('chpwd', false);
|
| 116 |
+
};
|
| 117 |
+
const apiForgotPwd = async () => {
|
| 118 |
+
if (!resetEmail) { alert('Enter email.'); return; }
|
| 119 |
+
setLoad('forgot', true);
|
| 120 |
+
setPwdResp(await req(`${AUTH}/forgot-password`, 'POST', { email: resetEmail }, false));
|
| 121 |
+
setLoad('forgot', false);
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
// Protected
|
| 125 |
+
const protCall = async (url, label, method = 'GET') => {
|
| 126 |
+
setLoad(label, true);
|
| 127 |
+
const r = await req(url, method);
|
| 128 |
+
setProtResp(r);
|
| 129 |
+
setLoad(label, false);
|
| 130 |
+
return r;
|
| 131 |
+
};
|
| 132 |
+
const apiLogout = async () => {
|
| 133 |
+
const r = await protCall(`${AUTH}/logout`, 'logout', 'POST');
|
| 134 |
+
if (r.ok) { localStorage.removeItem('token'); onTokenSave?.(); }
|
| 135 |
+
};
|
| 136 |
+
|
| 137 |
+
return (
|
| 138 |
+
<div className="grid-2">
|
| 139 |
+
{/* Register */}
|
| 140 |
+
<Card>
|
| 141 |
+
<CardHeader icon="📝">Register</CardHeader>
|
| 142 |
+
<FormGroup label="Email">
|
| 143 |
+
<input type="email" value={regEmail} onChange={e => setRegEmail(e.target.value)} placeholder="user@example.com" />
|
| 144 |
+
</FormGroup>
|
| 145 |
+
<FormGroup label="Password">
|
| 146 |
+
<input type="password" value={regPassword} onChange={e => setRegPassword(e.target.value)} placeholder="Min 8 chars, upper, lower, digit" />
|
| 147 |
+
</FormGroup>
|
| 148 |
+
<FormGroup label="Full Name">
|
| 149 |
+
<input type="text" value={regName} onChange={e => setRegName(e.target.value)} placeholder="Optional" />
|
| 150 |
+
</FormGroup>
|
| 151 |
+
<button className="btn btn-primary btn-full" onClick={apiRegister} disabled={loading.reg}>
|
| 152 |
+
{loading.reg ? 'Registering…' : 'Register'}
|
| 153 |
+
</button>
|
| 154 |
+
<ResponseBox result={regResp} />
|
| 155 |
+
</Card>
|
| 156 |
+
|
| 157 |
+
{/* Login */}
|
| 158 |
+
<Card>
|
| 159 |
+
<CardHeader icon="🔑">Login</CardHeader>
|
| 160 |
+
<FormGroup label="Email">
|
| 161 |
+
<input type="email" value={loginEmail} onChange={e => setLoginEmail(e.target.value)} placeholder="user@example.com" />
|
| 162 |
+
</FormGroup>
|
| 163 |
+
<FormGroup label="Password">
|
| 164 |
+
<input type="password" value={loginPassword} onChange={e => setLoginPassword(e.target.value)} placeholder="Password" />
|
| 165 |
+
</FormGroup>
|
| 166 |
+
<div className="flex gap-2">
|
| 167 |
+
<button className="btn btn-primary flex-1" onClick={apiLogin} disabled={loading.login}>
|
| 168 |
+
{loading.login ? '…' : 'Login'}
|
| 169 |
+
</button>
|
| 170 |
+
<button className="btn btn-ghost flex-1" onClick={apiAdaptiveLogin} disabled={loading.alogin}>
|
| 171 |
+
{loading.alogin ? '…' : 'Adaptive Login'}
|
| 172 |
+
</button>
|
| 173 |
+
</div>
|
| 174 |
+
<ResponseBox result={loginResp} />
|
| 175 |
+
</Card>
|
| 176 |
+
|
| 177 |
+
{/* 2FA */}
|
| 178 |
+
<Card>
|
| 179 |
+
<CardHeader icon="🔐">Two-Factor Auth (TOTP)</CardHeader>
|
| 180 |
+
<div className="flex flex-wrap gap-2 mb-3">
|
| 181 |
+
<button className="btn btn-primary btn-sm" onClick={api2faEnable} disabled={loading['2fa']}>Enable 2FA</button>
|
| 182 |
+
<button className="btn btn-ghost btn-sm" onClick={api2faStatus} disabled={loading['2fas']}>Status</button>
|
| 183 |
+
<button className="btn btn-danger btn-sm" onClick={api2faDisable} disabled={loading['2fad']}>Disable</button>
|
| 184 |
+
</div>
|
| 185 |
+
{qrCode && <img src={qrCode} className="qr-img" alt="TOTP QR Code" />}
|
| 186 |
+
<FormGroup label="TOTP Code">
|
| 187 |
+
<input type="text" value={totpCode} onChange={e => setTotpCode(e.target.value)} placeholder="6-digit code from authenticator" />
|
| 188 |
+
</FormGroup>
|
| 189 |
+
<button className="btn btn-success btn-full" onClick={api2faVerify} disabled={loading['2fav']}>
|
| 190 |
+
Verify & Activate
|
| 191 |
+
</button>
|
| 192 |
+
<ResponseBox result={tfaResp} />
|
| 193 |
+
</Card>
|
| 194 |
+
|
| 195 |
+
{/* Profile */}
|
| 196 |
+
<Card>
|
| 197 |
+
<CardHeader icon="👤">My Profile</CardHeader>
|
| 198 |
+
<div className="flex flex-wrap gap-2 mb-3">
|
| 199 |
+
<button className="btn btn-ghost btn-sm" onClick={() => profileCall(`${USER}/profile`, 'prof')}>Profile</button>
|
| 200 |
+
<button className="btn btn-ghost btn-sm" onClick={() => profileCall(`${USER}/security`, 'sec')}>Security</button>
|
| 201 |
+
<button className="btn btn-ghost btn-sm" onClick={() => profileCall(`${USER}/devices`, 'dev')}>Devices</button>
|
| 202 |
+
<button className="btn btn-ghost btn-sm" onClick={() => profileCall(`${USER}/sessions`, 'sess')}>Sessions</button>
|
| 203 |
+
<button className="btn btn-danger btn-sm" onClick={apiRevokeAll} disabled={loading.revoke}>Revoke All</button>
|
| 204 |
+
</div>
|
| 205 |
+
<ResponseBox result={profileResp} />
|
| 206 |
+
</Card>
|
| 207 |
+
|
| 208 |
+
{/* Password */}
|
| 209 |
+
<Card>
|
| 210 |
+
<CardHeader icon="🔒">Password Management</CardHeader>
|
| 211 |
+
<FormGroup label="Current Password">
|
| 212 |
+
<input type="password" value={curPwd} onChange={e => setCurPwd(e.target.value)} />
|
| 213 |
+
</FormGroup>
|
| 214 |
+
<FormGroup label="New Password">
|
| 215 |
+
<input type="password" value={newPwd} onChange={e => setNewPwd(e.target.value)} />
|
| 216 |
+
</FormGroup>
|
| 217 |
+
<button className="btn btn-warn btn-full" onClick={apiChangePwd} disabled={loading.chpwd}>
|
| 218 |
+
{loading.chpwd ? '…' : 'Change Password'}
|
| 219 |
+
</button>
|
| 220 |
+
<div className="border-t">
|
| 221 |
+
<FormGroup label="Email for Reset Link">
|
| 222 |
+
<input type="email" value={resetEmail} onChange={e => setResetEmail(e.target.value)} />
|
| 223 |
+
</FormGroup>
|
| 224 |
+
<button className="btn btn-ghost btn-full" onClick={apiForgotPwd} disabled={loading.forgot}>
|
| 225 |
+
{loading.forgot ? '…' : 'Send Reset Email'}
|
| 226 |
+
</button>
|
| 227 |
+
</div>
|
| 228 |
+
<ResponseBox result={pwdResp} />
|
| 229 |
+
</Card>
|
| 230 |
+
|
| 231 |
+
{/* Protected endpoints */}
|
| 232 |
+
<Card>
|
| 233 |
+
<CardHeader icon="🛡️">Protected Endpoints</CardHeader>
|
| 234 |
+
<p className="text-sm text-muted mb-3">
|
| 235 |
+
Test JWT-protected routes. Must have a valid token saved.
|
| 236 |
+
</p>
|
| 237 |
+
<div className="flex flex-wrap gap-2">
|
| 238 |
+
<button className="btn btn-success btn-sm" onClick={() => protCall(`${API_BASE}/protected`, 'prot')}>Test /protected</button>
|
| 239 |
+
<button className="btn btn-warn btn-sm" onClick={() => protCall(`${API_BASE}/admin-only`, 'admin')}>Test /admin-only</button>
|
| 240 |
+
<button className="btn btn-ghost btn-sm" onClick={() => protCall(`${USER}/risk-profile`, 'risk')}>Risk Profile</button>
|
| 241 |
+
<button className="btn btn-danger btn-sm" onClick={apiLogout}>Logout</button>
|
| 242 |
+
</div>
|
| 243 |
+
<ResponseBox result={protResp} />
|
| 244 |
+
</Card>
|
| 245 |
+
</div>
|
| 246 |
+
);
|
| 247 |
+
}
|
client/src/tabs/IntelTab.jsx
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
| 2 |
+
import { req, getToken, saveToken, ENDPOINTS } from '../api';
|
| 3 |
+
import { Card, CardHeader, Callout, FormGroup, ResponseBox } from '../components/ui';
|
| 4 |
+
|
| 5 |
+
const { AUTH, INTEL } = ENDPOINTS;
|
| 6 |
+
|
| 7 |
+
// ── Trust Gauge ───────────────────────────────────────────────────────────────
|
| 8 |
+
const TRUST_ARC = 204;
|
| 9 |
+
function TrustGauge({ score, label, color }) {
|
| 10 |
+
const offset = TRUST_ARC - (score / 100) * TRUST_ARC;
|
| 11 |
+
return (
|
| 12 |
+
<div style={{ textAlign: 'center' }}>
|
| 13 |
+
<svg width="160" height="100" viewBox="-5 -5 170 110" style={{ overflow: 'visible' }}>
|
| 14 |
+
<path d="M 10 80 A 65 65 0 0 1 140 80" stroke="#e2e8f0" strokeWidth="16" fill="none" />
|
| 15 |
+
<path
|
| 16 |
+
d="M 10 80 A 65 65 0 0 1 140 80"
|
| 17 |
+
stroke={color || '#16a34a'}
|
| 18 |
+
strokeWidth="16"
|
| 19 |
+
fill="none"
|
| 20 |
+
strokeDasharray={TRUST_ARC}
|
| 21 |
+
strokeDashoffset={offset}
|
| 22 |
+
strokeLinecap="round"
|
| 23 |
+
style={{ transition: 'stroke-dashoffset .6s, stroke .6s' }}
|
| 24 |
+
/>
|
| 25 |
+
<text x="75" y="77" textAnchor="middle" fontSize="28" fontWeight="800" fill={color || '#16a34a'}>
|
| 26 |
+
{Math.round(score)}
|
| 27 |
+
</text>
|
| 28 |
+
<text x="75" y="94" textAnchor="middle" fontSize="10" fill="#94a3b8">/ 100</text>
|
| 29 |
+
</svg>
|
| 30 |
+
<div className="trust-label" style={{ color: color || 'var(--muted)' }}>
|
| 31 |
+
{(label || 'loading').toUpperCase()}
|
| 32 |
+
</div>
|
| 33 |
+
</div>
|
| 34 |
+
);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// ── Behavior bar ──────────────────────────────────────────────────────────────
|
| 38 |
+
function BehaviorBar({ label, value }) {
|
| 39 |
+
const color = value > 0.7 ? '#16a34a' : value > 0.4 ? '#d97706' : '#dc2626';
|
| 40 |
+
return (
|
| 41 |
+
<div className="behavior-bar-group">
|
| 42 |
+
<div className="behavior-bar-header">
|
| 43 |
+
<span>{label}</span>
|
| 44 |
+
<span style={{ fontWeight: 700, color }}>{value.toFixed(2)}</span>
|
| 45 |
+
</div>
|
| 46 |
+
<div className="behavior-bar-track">
|
| 47 |
+
<div className="behavior-bar-fill" style={{ width: `${value * 100}%`, background: color }} />
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// ── Travel result renderer ────────────────────────────────────────────────────
|
| 54 |
+
function TravelResult({ data }) {
|
| 55 |
+
if (!data) return null;
|
| 56 |
+
const CMAP = { impossible:'#dc2626', suspicious:'#f97316', plausible:'#16a34a', same_area:'#16a34a', coords_unknown:'#94a3b8' };
|
| 57 |
+
const IMAP = { impossible:'🚨', suspicious:'⚠️', plausible:'✅', same_area:'✅', coords_unknown:'❓' };
|
| 58 |
+
const col = CMAP[data.verdict] || '#94a3b8';
|
| 59 |
+
const icon = IMAP[data.verdict] || '❓';
|
| 60 |
+
return (
|
| 61 |
+
<div style={{ background: `${col}18`, border: `1px solid ${col}`, borderRadius: 8, padding: 12, marginTop: 10 }}>
|
| 62 |
+
<div style={{ fontWeight: 700, fontSize: 15, color: col, marginBottom: 6 }}>
|
| 63 |
+
{icon} {(data.verdict || '').toUpperCase().replace('_', ' ')}
|
| 64 |
+
</div>
|
| 65 |
+
<div className="text-sm">{data.message}</div>
|
| 66 |
+
<div className="grid-3 mt-2" style={{ gap: 6 }}>
|
| 67 |
+
{[
|
| 68 |
+
{ v: data.distance_km || 0, l: 'Distance', u: 'km' },
|
| 69 |
+
{ v: Math.round(data.speed_kmh || 0), l: 'Speed', u: 'km/h' },
|
| 70 |
+
{ v: Math.round(data.time_gap_minutes || 0), l: 'Gap', u: 'min' },
|
| 71 |
+
].map(s => (
|
| 72 |
+
<div key={s.l}>
|
| 73 |
+
<div style={{ fontWeight: 700 }}>{s.v} {s.u}</div>
|
| 74 |
+
<div className="text-xs text-muted">{s.l}</div>
|
| 75 |
+
</div>
|
| 76 |
+
))}
|
| 77 |
+
</div>
|
| 78 |
+
{data.trust_delta < 0 && (
|
| 79 |
+
<div className="text-sm text-warn mt-2">⚠ Trust impact: {data.trust_delta} pts</div>
|
| 80 |
+
)}
|
| 81 |
+
</div>
|
| 82 |
+
);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// ── AI Anomaly result ─────────────────────────────────────────────────────────
|
| 86 |
+
function AnomalyResult({ data }) {
|
| 87 |
+
if (!data) return null;
|
| 88 |
+
return (
|
| 89 |
+
<div style={{ background: `${data.color}18`, border: `1px solid ${data.color}`, borderRadius: 8, padding: 12, marginTop: 10 }}>
|
| 90 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
| 91 |
+
<div style={{ fontWeight: 800, fontSize: 22, color: data.color }}>{data.anomaly_score.toFixed(1)} / 100</div>
|
| 92 |
+
<div style={{ fontSize: 11, fontWeight: 700, background: data.color, color: '#fff', padding: '2px 10px', borderRadius: 4 }}>
|
| 93 |
+
{data.classification}
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
<div className="text-xs text-muted mb-2">
|
| 97 |
+
Confidence: {(data.confidence * 100).toFixed(0)}% · Statistical Isolation Forest
|
| 98 |
+
</div>
|
| 99 |
+
{Object.entries(data.per_feature || {}).map(([fn, fs]) => {
|
| 100 |
+
const fc = fs > 60 ? '#dc2626' : fs > 30 ? '#d97706' : '#16a34a';
|
| 101 |
+
return (
|
| 102 |
+
<div key={fn} style={{ marginBottom: 6 }}>
|
| 103 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11 }}>
|
| 104 |
+
<span>{fn}</span><span style={{ color: fc }}>{fs.toFixed(1)}</span>
|
| 105 |
+
</div>
|
| 106 |
+
<div style={{ height: 5, background: '#e2e8f0', borderRadius: 3, overflow: 'hidden' }}>
|
| 107 |
+
<div style={{ height: '100%', width: `${fs}%`, background: fc, borderRadius: 3 }} />
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
);
|
| 111 |
+
})}
|
| 112 |
+
</div>
|
| 113 |
+
);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
export default function IntelTab({ onTokenSave }) {
|
| 117 |
+
// Login status
|
| 118 |
+
const [loginStatus, setLoginStatus] = useState('');
|
| 119 |
+
// Trust
|
| 120 |
+
const [trust, setTrust] = useState({ score: 0, label: 'loading', color: '#94a3b8' });
|
| 121 |
+
const [trustHistory, setTrustHist] = useState([]);
|
| 122 |
+
const [trustResp, setTrustResp] = useState(null);
|
| 123 |
+
// Behavior
|
| 124 |
+
const [collecting, setCollecting] = useState(false);
|
| 125 |
+
const [bhScores, setBhScores] = useState({ te: 0.5, ml: 0.6, sv: 0.5 });
|
| 126 |
+
const [collectStatus,setCollStatus] = useState('');
|
| 127 |
+
const [behaviorResp, setBehavResp] = useState(null);
|
| 128 |
+
// Travel
|
| 129 |
+
const [cities, setCities] = useState([]);
|
| 130 |
+
const [travelFrom, setTravelFrom] = useState('New York');
|
| 131 |
+
const [travelTo, setTravelTo] = useState('Moscow');
|
| 132 |
+
const [travelHours, setTravelHours] = useState('2');
|
| 133 |
+
const [travelResult, setTravelResult]= useState(null);
|
| 134 |
+
// AI Anomaly
|
| 135 |
+
const [aiTyping, setAiTyping] = useState('0.70');
|
| 136 |
+
const [aiMouse, setAiMouse] = useState('0.62');
|
| 137 |
+
const [aiScroll, setAiScroll] = useState('0.48');
|
| 138 |
+
const [aiHour, setAiHour] = useState('0.55');
|
| 139 |
+
const [aiFailed, setAiFailed] = useState('0.00');
|
| 140 |
+
const [anomResult, setAnomResult]= useState(null);
|
| 141 |
+
// Challenge
|
| 142 |
+
const [showChallenge, setShowChallenge] = useState(false);
|
| 143 |
+
const [challengeQ, setChallengeQ] = useState('');
|
| 144 |
+
const [challengeAnswer, setChallengeAnswer] = useState('');
|
| 145 |
+
const [challengeMsg, setChallengeMsg] = useState('');
|
| 146 |
+
const [challengeId, setChallengeId] = useState(null);
|
| 147 |
+
const [challengeResp, setChallengeResp] = useState(null);
|
| 148 |
+
// Explain
|
| 149 |
+
const [expLoc, setExpLoc] = useState('85');
|
| 150 |
+
const [expDev, setExpDev] = useState('15');
|
| 151 |
+
const [expTime, setExpTime] = useState('10');
|
| 152 |
+
const [expVel, setExpVel] = useState('5');
|
| 153 |
+
const [expBeh, setExpBeh] = useState('20');
|
| 154 |
+
const [expLevel, setExpLevel] = useState('2');
|
| 155 |
+
const [explainResult, setExplainResult] = useState(null);
|
| 156 |
+
// Session audit
|
| 157 |
+
const [sessionResp, setSessionResp] = useState(null);
|
| 158 |
+
// Loading
|
| 159 |
+
const [loading, setLoading] = useState({});
|
| 160 |
+
const setLoad = (k, v) => setLoading(p => ({ ...p, [k]: v }));
|
| 161 |
+
|
| 162 |
+
// ── Behavior refs ─────────────────────────────────────────────────────────
|
| 163 |
+
const bhKeyTimes = useRef([]);
|
| 164 |
+
const bhMousePts = useRef([]);
|
| 165 |
+
const bhScrollDs = useRef([]);
|
| 166 |
+
const bhLastKey = useRef(0);
|
| 167 |
+
const bhLastMouse = useRef(0);
|
| 168 |
+
|
| 169 |
+
const onKeyDown = useCallback(e => {
|
| 170 |
+
const now = performance.now();
|
| 171 |
+
if (bhLastKey.current > 0) bhKeyTimes.current.push(now - bhLastKey.current);
|
| 172 |
+
bhLastKey.current = now;
|
| 173 |
+
}, []);
|
| 174 |
+
const onMouseMove = useCallback(e => {
|
| 175 |
+
const now = performance.now();
|
| 176 |
+
if (now - bhLastMouse.current < 50) return;
|
| 177 |
+
bhLastMouse.current = now;
|
| 178 |
+
bhMousePts.current.push([e.clientX, e.clientY]);
|
| 179 |
+
}, []);
|
| 180 |
+
const onScroll = useCallback(e => {
|
| 181 |
+
const d = e.target?.scrollTop != null ? Math.abs(e.target.scrollTop) : window.scrollY;
|
| 182 |
+
bhScrollDs.current.push(d);
|
| 183 |
+
}, []);
|
| 184 |
+
|
| 185 |
+
const computeScores = useCallback(() => {
|
| 186 |
+
// Typing entropy (coefficient of variation)
|
| 187 |
+
let te = 0.5;
|
| 188 |
+
const kt = bhKeyTimes.current;
|
| 189 |
+
if (kt.length >= 3) {
|
| 190 |
+
const mean = kt.reduce((a, b) => a + b, 0) / kt.length;
|
| 191 |
+
const std = Math.sqrt(kt.reduce((s, v) => s + (v - mean) ** 2, 0) / kt.length);
|
| 192 |
+
const cv = std / (mean + 1e-6);
|
| 193 |
+
te = cv < 0.05 ? 0.10 : cv < 0.20 ? 0.30 + cv * 1.5 : cv < 0.90 ? 0.50 + (cv - 0.20) * 0.7 : Math.max(0.10, 1.0 - (cv - 0.90) * 0.8);
|
| 194 |
+
te = Math.max(0, Math.min(1, te));
|
| 195 |
+
}
|
| 196 |
+
// Mouse linearity
|
| 197 |
+
let ml = 0.6;
|
| 198 |
+
const mp = bhMousePts.current;
|
| 199 |
+
if (mp.length >= 3) {
|
| 200 |
+
let totalD = 0;
|
| 201 |
+
for (let i = 1; i < mp.length; i++) {
|
| 202 |
+
const dx = mp[i][0] - mp[i-1][0], dy = mp[i][1] - mp[i-1][1];
|
| 203 |
+
totalD += Math.sqrt(dx * dx + dy * dy);
|
| 204 |
+
}
|
| 205 |
+
const dxA = mp[mp.length-1][0] - mp[0][0], dyA = mp[mp.length-1][1] - mp[0][1];
|
| 206 |
+
const straightD = Math.sqrt(dxA * dxA + dyA * dyA);
|
| 207 |
+
ml = Math.max(0, Math.min(1, 1.0 - Math.abs((totalD > 0 ? straightD / totalD : 0) - 0.6) * 1.5));
|
| 208 |
+
}
|
| 209 |
+
// Scroll variance
|
| 210 |
+
let sv = 0.5;
|
| 211 |
+
const sd = bhScrollDs.current;
|
| 212 |
+
if (sd.length >= 2) {
|
| 213 |
+
const sm = sd.reduce((a, b) => a + b, 0) / sd.length;
|
| 214 |
+
const ss = Math.sqrt(sd.reduce((s, v) => s + (v - sm) ** 2, 0) / sd.length);
|
| 215 |
+
sv = Math.min(1, ss / 200);
|
| 216 |
+
}
|
| 217 |
+
return { te, ml, sv, lr: Math.max(0, 1.0 - (0.40 * te + 0.35 * ml + 0.25 * sv)) };
|
| 218 |
+
}, []);
|
| 219 |
+
|
| 220 |
+
const startCollecting = () => {
|
| 221 |
+
bhKeyTimes.current = []; bhMousePts.current = []; bhScrollDs.current = [];
|
| 222 |
+
bhLastKey.current = 0; bhLastMouse.current = 0;
|
| 223 |
+
document.addEventListener('keydown', onKeyDown);
|
| 224 |
+
document.addEventListener('mousemove', onMouseMove);
|
| 225 |
+
document.addEventListener('scroll', onScroll, true);
|
| 226 |
+
setCollecting(true);
|
| 227 |
+
setCollStatus('Collecting… (type, move mouse, scroll)');
|
| 228 |
+
setBhScores({ te: 0.5, ml: 0.6, sv: 0.5 });
|
| 229 |
+
};
|
| 230 |
+
const stopCollecting = () => {
|
| 231 |
+
document.removeEventListener('keydown', onKeyDown);
|
| 232 |
+
document.removeEventListener('mousemove', onMouseMove);
|
| 233 |
+
document.removeEventListener('scroll', onScroll, true);
|
| 234 |
+
const s = computeScores();
|
| 235 |
+
setBhScores(s);
|
| 236 |
+
setCollecting(false);
|
| 237 |
+
setCollStatus(`Done — Keys:${bhKeyTimes.current.length} Mouse:${bhMousePts.current.length} Scrolls:${bhScrollDs.current.length}`);
|
| 238 |
+
};
|
| 239 |
+
useEffect(() => () => {
|
| 240 |
+
document.removeEventListener('keydown', onKeyDown);
|
| 241 |
+
document.removeEventListener('mousemove', onMouseMove);
|
| 242 |
+
document.removeEventListener('scroll', onScroll, true);
|
| 243 |
+
}, [onKeyDown, onMouseMove, onScroll]);
|
| 244 |
+
|
| 245 |
+
// ── Cities ────────────────────────────────────────────────────────────────
|
| 246 |
+
useEffect(() => {
|
| 247 |
+
req(`${INTEL}/demo/city-list`, 'GET', null, false).then(r => {
|
| 248 |
+
if (r.ok && r.data?.cities) setCities(r.data.cities.map(c => c.name));
|
| 249 |
+
});
|
| 250 |
+
if (getToken()) intelGetTrust();
|
| 251 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 252 |
+
}, []);
|
| 253 |
+
|
| 254 |
+
// ── Trust ─────────────────────────────────────────────────────────────────
|
| 255 |
+
const intelGetTrust = async () => {
|
| 256 |
+
if (!getToken()) return;
|
| 257 |
+
setLoad('trust', true);
|
| 258 |
+
const r = await req(`${INTEL}/trust-score`);
|
| 259 |
+
setTrustResp(r);
|
| 260 |
+
if (r.ok && r.data) {
|
| 261 |
+
setTrust({ score: r.data.trust_score, label: r.data.label, color: r.data.color });
|
| 262 |
+
setTrustHist(r.data.history || []);
|
| 263 |
+
}
|
| 264 |
+
setLoad('trust', false);
|
| 265 |
+
};
|
| 266 |
+
const intelVerify = async () => {
|
| 267 |
+
if (!getToken()) { alert('Login first.'); return; }
|
| 268 |
+
setLoad('verify', true);
|
| 269 |
+
const r = await req(`${INTEL}/continuous-verify`, 'POST', {});
|
| 270 |
+
setTrustResp(r);
|
| 271 |
+
if (r.ok) setTrust({ score: r.data.trust_score, label: r.data.label, color: r.data.color });
|
| 272 |
+
setLoad('verify', false);
|
| 273 |
+
};
|
| 274 |
+
const intelDropTrust = async () => {
|
| 275 |
+
if (!getToken()) { alert('Login first (use Quick Login above).'); return; }
|
| 276 |
+
setLoad('drop', true);
|
| 277 |
+
const r = await req(`${INTEL}/simulate-trust-drop`, 'POST', { target_score: 25, reason: 'Manual demo drop' });
|
| 278 |
+
setTrustResp(r);
|
| 279 |
+
if (r.ok && r.data) {
|
| 280 |
+
setTrust({ score: r.data.new_trust, label: r.data.trust_label, color: r.data.trust_color });
|
| 281 |
+
}
|
| 282 |
+
setLoad('drop', false);
|
| 283 |
+
};
|
| 284 |
+
|
| 285 |
+
// ── Quick Login ───────────────────────────────────────────────────────────
|
| 286 |
+
const quickLogin = async () => {
|
| 287 |
+
setLoad('ql', true);
|
| 288 |
+
const r = await req(`${AUTH}/login`, 'POST', { email: 'demo.user@adaptive.demo', password: 'DemoUser@123!' }, false);
|
| 289 |
+
if (r.ok && r.data?.access_token) {
|
| 290 |
+
saveToken(r.data.access_token);
|
| 291 |
+
onTokenSave?.();
|
| 292 |
+
setLoginStatus('✅ Logged in as demo.user@adaptive.demo');
|
| 293 |
+
await intelGetTrust();
|
| 294 |
+
} else {
|
| 295 |
+
setLoginStatus('❌ Login failed — run Setup in Scenario 1 first.');
|
| 296 |
+
}
|
| 297 |
+
setLoad('ql', false);
|
| 298 |
+
};
|
| 299 |
+
|
| 300 |
+
// ── Behavior Send ─────────────────────────────────────────────────────────
|
| 301 |
+
const sendBehavior = async () => {
|
| 302 |
+
if (!getToken()) { alert('Login first.'); return; }
|
| 303 |
+
const s = computeScores();
|
| 304 |
+
setBhScores(s);
|
| 305 |
+
setLoad('beh', true);
|
| 306 |
+
const r = await req(`${INTEL}/behavior-signal`, 'POST', {
|
| 307 |
+
typing_entropy: s.te, mouse_linearity: s.ml, scroll_variance: s.sv, local_risk_score: s.lr,
|
| 308 |
+
});
|
| 309 |
+
setBehavResp(r);
|
| 310 |
+
if (r.ok && r.data?.trust) {
|
| 311 |
+
setTrust({ score: r.data.trust.score, label: r.data.trust.label, color: r.data.trust.color });
|
| 312 |
+
}
|
| 313 |
+
setLoad('beh', false);
|
| 314 |
+
};
|
| 315 |
+
|
| 316 |
+
// ── Travel ────────────────────────────────────────────────────────────────
|
| 317 |
+
const checkTravel = async () => {
|
| 318 |
+
setLoad('travel', true);
|
| 319 |
+
const r = await req(`${INTEL}/demo/impossible-travel`, 'POST', {
|
| 320 |
+
from_city: travelFrom, to_city: travelTo,
|
| 321 |
+
time_gap_hours: parseFloat(travelHours), from_country: '', to_country: '',
|
| 322 |
+
}, false);
|
| 323 |
+
setTravelResult(r.ok ? r.data : null);
|
| 324 |
+
setLoad('travel', false);
|
| 325 |
+
};
|
| 326 |
+
|
| 327 |
+
// ── AI Anomaly ────────────────────────────────────────────────────────────
|
| 328 |
+
const scoreAnomaly = async () => {
|
| 329 |
+
setLoad('ai', true);
|
| 330 |
+
const r = await req(`${INTEL}/demo/anomaly-score`, 'POST', {
|
| 331 |
+
typing_entropy: parseFloat(aiTyping),
|
| 332 |
+
mouse_linearity: parseFloat(aiMouse),
|
| 333 |
+
scroll_variance: parseFloat(aiScroll),
|
| 334 |
+
hour_normalized: parseFloat(aiHour),
|
| 335 |
+
failed_attempts_norm: parseFloat(aiFailed),
|
| 336 |
+
}, false);
|
| 337 |
+
setAnomResult(r.ok ? r.data : null);
|
| 338 |
+
setLoad('ai', false);
|
| 339 |
+
};
|
| 340 |
+
|
| 341 |
+
// ── Micro-Challenge ───────────────────────────────────────────────────────
|
| 342 |
+
const generateChallenge = async () => {
|
| 343 |
+
if (!getToken()) {
|
| 344 |
+
const a = Math.floor(Math.random() * 9) + 2, b = Math.floor(Math.random() * 9) + 2;
|
| 345 |
+
setChallengeId('demo-no-auth');
|
| 346 |
+
setChallengeQ(`What is ${a} × ${b} ?`);
|
| 347 |
+
setChallengeAnswer('');
|
| 348 |
+
setChallengeMsg('(Demo mode — login to persist trust changes)');
|
| 349 |
+
setShowChallenge(true);
|
| 350 |
+
return;
|
| 351 |
+
}
|
| 352 |
+
setLoad('ch', true);
|
| 353 |
+
const r = await req(`${INTEL}/micro-challenge/generate`, 'POST', {});
|
| 354 |
+
setChallengeResp(r);
|
| 355 |
+
if (r.ok && r.data?.challenge) {
|
| 356 |
+
setChallengeId(r.data.challenge.challenge_id);
|
| 357 |
+
setChallengeQ(r.data.challenge.question);
|
| 358 |
+
setChallengeAnswer('');
|
| 359 |
+
setChallengeMsg(r.data.challenge_needed ? '' : 'ℹ Trust is healthy — showing challenge for demo purposes.');
|
| 360 |
+
setShowChallenge(true);
|
| 361 |
+
}
|
| 362 |
+
setLoad('ch', false);
|
| 363 |
+
};
|
| 364 |
+
|
| 365 |
+
const verifyChallenge = async () => {
|
| 366 |
+
if (!challengeAnswer.trim()) { alert('Enter your answer.'); return; }
|
| 367 |
+
if (!challengeId || challengeId === 'demo-no-auth') {
|
| 368 |
+
setChallengeMsg('✅ Submitted (login to update real trust score).');
|
| 369 |
+
setShowChallenge(false);
|
| 370 |
+
return;
|
| 371 |
+
}
|
| 372 |
+
setLoad('chv', true);
|
| 373 |
+
const r = await req(`${INTEL}/micro-challenge/verify`, 'POST', { challenge_id: challengeId, response: challengeAnswer });
|
| 374 |
+
setChallengeResp(r);
|
| 375 |
+
if (r.ok && r.data) {
|
| 376 |
+
setChallengeMsg(r.data.reason);
|
| 377 |
+
if (r.data.correct) {
|
| 378 |
+
setShowChallenge(false);
|
| 379 |
+
setTrust({ score: r.data.new_trust, label: r.data.trust_label, color: r.data.trust_color });
|
| 380 |
+
} else {
|
| 381 |
+
setChallengeAnswer('');
|
| 382 |
+
}
|
| 383 |
+
}
|
| 384 |
+
setLoad('chv', false);
|
| 385 |
+
};
|
| 386 |
+
|
| 387 |
+
// ── Explain ───────────────────────────────────────────────────────────────
|
| 388 |
+
const explainRisk = async () => {
|
| 389 |
+
setLoad('exp', true);
|
| 390 |
+
const r = await req(`${INTEL}/demo/explain`, 'POST', {
|
| 391 |
+
location_score: parseFloat(expLoc), device_score: parseFloat(expDev),
|
| 392 |
+
time_score: parseFloat(expTime), velocity_score: parseFloat(expVel),
|
| 393 |
+
behavior_score: parseFloat(expBeh), security_level: parseInt(expLevel),
|
| 394 |
+
risk_level: 'medium',
|
| 395 |
+
}, false);
|
| 396 |
+
setExplainResult(r.ok ? r.data : null);
|
| 397 |
+
setLoad('exp', false);
|
| 398 |
+
};
|
| 399 |
+
|
| 400 |
+
return (
|
| 401 |
+
<div>
|
| 402 |
+
<Callout type="info">
|
| 403 |
+
<strong>🧠 Session Intelligence — 8 Advanced Security Features</strong><br />
|
| 404 |
+
Continuous Verification • Behavioral Intelligence • Dynamic Trust Score •
|
| 405 |
+
Micro-Challenges • Explainability • AI Anomaly Detection • Impossible Travel •
|
| 406 |
+
Privacy-First Design.
|
| 407 |
+
</Callout>
|
| 408 |
+
|
| 409 |
+
{/* Quick Login */}
|
| 410 |
+
<Card>
|
| 411 |
+
<CardHeader icon="🔑">Session Authentication</CardHeader>
|
| 412 |
+
<div className="flex items-center gap-3 flex-wrap">
|
| 413 |
+
<span className="text-sm text-muted">Protected features require a JWT token.</span>
|
| 414 |
+
<button className="btn btn-primary btn-sm" onClick={quickLogin} disabled={loading.ql}>
|
| 415 |
+
{loading.ql ? '…' : '⚡ Quick Login (demo user)'}
|
| 416 |
+
</button>
|
| 417 |
+
{loginStatus && (
|
| 418 |
+
<span className="text-sm" style={{ color: loginStatus.startsWith('✅') ? 'var(--success)' : 'var(--danger)' }}>
|
| 419 |
+
{loginStatus}
|
| 420 |
+
</span>
|
| 421 |
+
)}
|
| 422 |
+
</div>
|
| 423 |
+
</Card>
|
| 424 |
+
|
| 425 |
+
{/* Trust Score */}
|
| 426 |
+
<Card>
|
| 427 |
+
<CardHeader icon="🛡️">Dynamic Trust Score & Continuous Verification</CardHeader>
|
| 428 |
+
<div className="flex gap-4 flex-wrap items-start">
|
| 429 |
+
<TrustGauge score={trust.score} label={trust.label} color={trust.color} />
|
| 430 |
+
<div style={{ flex: 1, minWidth: 180 }}>
|
| 431 |
+
<div className="flex gap-2 flex-wrap mb-3">
|
| 432 |
+
<button className="btn btn-ghost btn-sm" onClick={intelGetTrust} disabled={loading.trust}>🔄 Refresh</button>
|
| 433 |
+
<button className="btn btn-ghost btn-sm" onClick={intelVerify} disabled={loading.verify}>✔ Verify Now</button>
|
| 434 |
+
<button className="btn btn-warn btn-sm" onClick={intelDropTrust} disabled={loading.drop}>🔽 Drop to 25</button>
|
| 435 |
+
</div>
|
| 436 |
+
{trustHistory.length > 0 && (
|
| 437 |
+
<div>
|
| 438 |
+
<div className="text-xs font-600 uppercase letter-wide text-muted mb-2">Recent Trust Events</div>
|
| 439 |
+
<div style={{ maxHeight: 130, overflowY: 'auto' }}>
|
| 440 |
+
{[...trustHistory].reverse().slice(0, 20).map((e, i) => (
|
| 441 |
+
<div key={i} className="flex justify-between text-xs" style={{ borderBottom: '1px solid var(--border)', padding: '2px 0' }}>
|
| 442 |
+
<span className="text-muted">{e.event_type}</span>
|
| 443 |
+
<span style={{ color: e.delta >= 0 ? 'var(--success)' : 'var(--danger)' }}>
|
| 444 |
+
{e.delta >= 0 ? '+' : ''}{e.delta.toFixed(1)} → {e.score.toFixed(0)}
|
| 445 |
+
</span>
|
| 446 |
+
</div>
|
| 447 |
+
))}
|
| 448 |
+
</div>
|
| 449 |
+
</div>
|
| 450 |
+
)}
|
| 451 |
+
</div>
|
| 452 |
+
</div>
|
| 453 |
+
<ResponseBox result={trustResp} />
|
| 454 |
+
</Card>
|
| 455 |
+
|
| 456 |
+
{/* Behavior Intelligence */}
|
| 457 |
+
<Card>
|
| 458 |
+
<CardHeader icon="🔒">Privacy-First Behavioral Intelligence</CardHeader>
|
| 459 |
+
<div className="callout callout-info text-sm mb-3" style={{ padding: '8px 12px' }}>
|
| 460 |
+
<strong>🔒 Privacy-First:</strong> Keystroke timings, mouse coords and scroll deltas are processed{' '}
|
| 461 |
+
<em>entirely in-browser</em>. Only the aggregated 0–1 scores are sent to the server.
|
| 462 |
+
</div>
|
| 463 |
+
<div className="flex gap-2 flex-wrap mb-3 items-center">
|
| 464 |
+
<button
|
| 465 |
+
className={`btn btn-sm ${collecting ? 'btn-danger' : 'btn-success'}`}
|
| 466 |
+
onClick={collecting ? stopCollecting : startCollecting}
|
| 467 |
+
>
|
| 468 |
+
{collecting ? '⏹ Stop Collecting' : '▶ Start Collecting'}
|
| 469 |
+
</button>
|
| 470 |
+
<button className="btn btn-primary btn-sm" onClick={sendBehavior} disabled={loading.beh}>
|
| 471 |
+
📤 Send Signals
|
| 472 |
+
</button>
|
| 473 |
+
{collectStatus && <span className="text-xs text-muted">{collectStatus}</span>}
|
| 474 |
+
</div>
|
| 475 |
+
<BehaviorBar label="⌨️ Typing Entropy (1.0 = human-like rhythm)" value={bhScores.te} />
|
| 476 |
+
<BehaviorBar label="🖱️ Mouse Linearity (1.0 = curved/natural)" value={bhScores.ml} />
|
| 477 |
+
<BehaviorBar label="📜 Scroll Variance (0.5 = organic human rhythm)" value={bhScores.sv} />
|
| 478 |
+
<ResponseBox result={behaviorResp} />
|
| 479 |
+
</Card>
|
| 480 |
+
|
| 481 |
+
<div className="grid-2">
|
| 482 |
+
{/* Impossible Travel */}
|
| 483 |
+
<Card style={{ margin: 0 }}>
|
| 484 |
+
<CardHeader icon="✈️">Impossible Travel Detector</CardHeader>
|
| 485 |
+
<div className="grid-2 mb-2">
|
| 486 |
+
<FormGroup label="FROM City">
|
| 487 |
+
<select value={travelFrom} onChange={e => setTravelFrom(e.target.value)}>
|
| 488 |
+
{cities.map(c => <option key={c}>{c}</option>)}
|
| 489 |
+
</select>
|
| 490 |
+
</FormGroup>
|
| 491 |
+
<FormGroup label="TO City">
|
| 492 |
+
<select value={travelTo} onChange={e => setTravelTo(e.target.value)}>
|
| 493 |
+
{cities.map(c => <option key={c}>{c}</option>)}
|
| 494 |
+
</select>
|
| 495 |
+
</FormGroup>
|
| 496 |
+
</div>
|
| 497 |
+
<FormGroup label="Time gap (hours)">
|
| 498 |
+
<input type="number" value={travelHours} onChange={e => setTravelHours(e.target.value)} min="0.01" step="0.5" />
|
| 499 |
+
</FormGroup>
|
| 500 |
+
<button className="btn btn-primary btn-sm btn-full" onClick={checkTravel} disabled={loading.travel}>
|
| 501 |
+
{loading.travel ? '…' : '📏 Calculate Travel Risk'}
|
| 502 |
+
</button>
|
| 503 |
+
<TravelResult data={travelResult} />
|
| 504 |
+
</Card>
|
| 505 |
+
|
| 506 |
+
{/* AI Anomaly Scorer */}
|
| 507 |
+
<Card style={{ margin: 0 }}>
|
| 508 |
+
<CardHeader icon="🤖">AI Anomaly Scorer</CardHeader>
|
| 509 |
+
{[
|
| 510 |
+
{ label: 'Typing entropy', val: aiTyping, set: setAiTyping },
|
| 511 |
+
{ label: 'Mouse linearity', val: aiMouse, set: setAiMouse },
|
| 512 |
+
{ label: 'Scroll variance', val: aiScroll, set: setAiScroll },
|
| 513 |
+
{ label: 'Hour normalized', val: aiHour, set: setAiHour },
|
| 514 |
+
{ label: 'Failed attempts (÷20)', val: aiFailed, set: setAiFailed },
|
| 515 |
+
].map(f => (
|
| 516 |
+
<div key={f.label} className="flex items-center justify-between gap-2 mb-2">
|
| 517 |
+
<span className="text-sm text-2">{f.label}</span>
|
| 518 |
+
<input
|
| 519 |
+
type="number" value={f.val} onChange={e => f.set(e.target.value)}
|
| 520 |
+
min="0" max="1" step="0.05"
|
| 521 |
+
style={{ width: 72, textAlign: 'right', padding: '3px 6px' }}
|
| 522 |
+
/>
|
| 523 |
+
</div>
|
| 524 |
+
))}
|
| 525 |
+
<button className="btn btn-primary btn-sm btn-full mt-2" onClick={scoreAnomaly} disabled={loading.ai}>
|
| 526 |
+
{loading.ai ? '…' : '🧠 Score with AI'}
|
| 527 |
+
</button>
|
| 528 |
+
<AnomalyResult data={anomResult} />
|
| 529 |
+
</Card>
|
| 530 |
+
</div>
|
| 531 |
+
|
| 532 |
+
{/* Micro-Challenges */}
|
| 533 |
+
<Card>
|
| 534 |
+
<CardHeader icon="🧩">Low-Friction Micro-Challenges</CardHeader>
|
| 535 |
+
<p className="text-sm text-muted mb-3">
|
| 536 |
+
Challenges fire <em>only when trust drops below 40</em> — never interrupts a trusted session.
|
| 537 |
+
</p>
|
| 538 |
+
<div className="flex gap-2 flex-wrap mb-3">
|
| 539 |
+
<button className="btn btn-warn btn-sm" onClick={intelDropTrust} disabled={loading.drop}>🔽 Drop Trust to 25</button>
|
| 540 |
+
<button className="btn btn-primary btn-sm" onClick={generateChallenge} disabled={loading.ch}>🧩 Generate Challenge</button>
|
| 541 |
+
</div>
|
| 542 |
+
{showChallenge && (
|
| 543 |
+
<div className="callout callout-info">
|
| 544 |
+
<div className="font-bold mb-2" style={{ fontSize: 16 }}>{challengeQ}</div>
|
| 545 |
+
<div className="flex gap-2 items-center">
|
| 546 |
+
<input
|
| 547 |
+
type="text"
|
| 548 |
+
value={challengeAnswer}
|
| 549 |
+
onChange={e => setChallengeAnswer(e.target.value)}
|
| 550 |
+
placeholder="Your answer…"
|
| 551 |
+
style={{ width: 160 }}
|
| 552 |
+
/>
|
| 553 |
+
<button className="btn btn-success btn-sm" onClick={verifyChallenge} disabled={loading.chv}>
|
| 554 |
+
{loading.chv ? '…' : '✔ Verify'}
|
| 555 |
+
</button>
|
| 556 |
+
</div>
|
| 557 |
+
{challengeMsg && <div className="text-sm mt-2">{challengeMsg}</div>}
|
| 558 |
+
</div>
|
| 559 |
+
)}
|
| 560 |
+
<ResponseBox result={challengeResp} />
|
| 561 |
+
</Card>
|
| 562 |
+
|
| 563 |
+
{/* Explainability */}
|
| 564 |
+
<Card>
|
| 565 |
+
<CardHeader icon="📊">Explainable Risk Transparency</CardHeader>
|
| 566 |
+
<p className="text-sm text-muted mb-3">
|
| 567 |
+
Submit factor scores and see exactly which signals contributed and why — with model weights.
|
| 568 |
+
</p>
|
| 569 |
+
<div className="grid-3 mb-3">
|
| 570 |
+
{[
|
| 571 |
+
{ label: '🌍 Location (0-100)', val: expLoc, set: setExpLoc, max: 100 },
|
| 572 |
+
{ label: '💻 Device', val: expDev, set: setExpDev, max: 100 },
|
| 573 |
+
{ label: '🕐 Time', val: expTime, set: setExpTime, max: 100 },
|
| 574 |
+
{ label: '⚡ Velocity', val: expVel, set: setExpVel, max: 100 },
|
| 575 |
+
{ label: '🧠 Behavior', val: expBeh, set: setExpBeh, max: 100 },
|
| 576 |
+
{ label: '🔒 Security level (0-4)',val: expLevel, set: setExpLevel, max: 4 },
|
| 577 |
+
].map(f => (
|
| 578 |
+
<FormGroup key={f.label} label={f.label}>
|
| 579 |
+
<input type="number" value={f.val} onChange={e => f.set(e.target.value)} min="0" max={f.max} />
|
| 580 |
+
</FormGroup>
|
| 581 |
+
))}
|
| 582 |
+
</div>
|
| 583 |
+
<button className="btn btn-primary btn-sm" onClick={explainRisk} disabled={loading.exp}>
|
| 584 |
+
{loading.exp ? '…' : '🔍 Generate Explanation'}
|
| 585 |
+
</button>
|
| 586 |
+
{explainResult && (
|
| 587 |
+
<div className="mt-3">
|
| 588 |
+
<div className="text-sm text-muted mb-2">
|
| 589 |
+
🔍 Audit ID: <code>{explainResult.audit_id}</code> ·
|
| 590 |
+
Confidence: {(explainResult.confidence * 100).toFixed(0)}% ·
|
| 591 |
+
Action: <em>{explainResult.action}</em>
|
| 592 |
+
</div>
|
| 593 |
+
<div className="resp-box" style={{ background: 'var(--surface-2)' }}>{explainResult.summary}</div>
|
| 594 |
+
{(explainResult.factors || []).map(f => {
|
| 595 |
+
const col = f.status === 'anomalous' ? '#dc2626' : '#16a34a';
|
| 596 |
+
const bar = Math.min(100, Math.max(0, Math.abs(f.contribution) * 4));
|
| 597 |
+
return (
|
| 598 |
+
<div key={f.factor} className="factor-row mt-2">
|
| 599 |
+
<div className="factor-label">
|
| 600 |
+
<span>{f.icon} <strong>{f.factor}</strong> <span className="text-xs text-muted">w:{f.model_weight}</span></span>
|
| 601 |
+
<span style={{ color: col }}>{f.contribution >= 0 ? '+' : ''}{f.contribution.toFixed(1)}</span>
|
| 602 |
+
</div>
|
| 603 |
+
<div className="factor-bar-wrap">
|
| 604 |
+
<div className="factor-bar" style={{ width: `${bar}%`, background: col }} />
|
| 605 |
+
</div>
|
| 606 |
+
<div className="text-xs text-muted mt-1">{f.detail}</div>
|
| 607 |
+
</div>
|
| 608 |
+
);
|
| 609 |
+
})}
|
| 610 |
+
</div>
|
| 611 |
+
)}
|
| 612 |
+
</Card>
|
| 613 |
+
|
| 614 |
+
{/* Session Audit Trail */}
|
| 615 |
+
<Card>
|
| 616 |
+
<CardHeader icon="📋">Session Audit Trail <span className="text-sm text-muted font-400">(requires login)</span></CardHeader>
|
| 617 |
+
<button className="btn btn-ghost btn-sm mb-2" onClick={async () => {
|
| 618 |
+
if (!getToken()) { alert('Login first.'); return; }
|
| 619 |
+
setLoad('audit', true);
|
| 620 |
+
setSessionResp(await req(`${INTEL}/explain`));
|
| 621 |
+
setLoad('audit', false);
|
| 622 |
+
}} disabled={loading.audit}>
|
| 623 |
+
{loading.audit ? '…' : '📄 Fetch My Session Events'}
|
| 624 |
+
</button>
|
| 625 |
+
<ResponseBox result={sessionResp} />
|
| 626 |
+
</Card>
|
| 627 |
+
</div>
|
| 628 |
+
);
|
| 629 |
+
}
|
client/src/tabs/Scenario1Tab.jsx
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import { req, saveToken, ENDPOINTS } from '../api';
|
| 3 |
+
import { Card, CardHeader, Callout, FormGroup, ResponseBox, StepBar, Tag } from '../components/ui';
|
| 4 |
+
import { RiskVizCard } from '../components/RiskViz';
|
| 5 |
+
|
| 6 |
+
const DEMO = ENDPOINTS.DEMO;
|
| 7 |
+
|
| 8 |
+
const STEPS = [
|
| 9 |
+
{ label: 'Setup', sub: 'Create demo user' },
|
| 10 |
+
{ label: 'Normal Login', sub: 'Known context' },
|
| 11 |
+
{ label: 'Suspicious Login', sub: 'Unknown context' },
|
| 12 |
+
{ label: 'Verify Challenge', sub: 'Step-up auth' },
|
| 13 |
+
];
|
| 14 |
+
|
| 15 |
+
export default function Scenario1Tab({ onTokenSave }) {
|
| 16 |
+
const [step, setStep] = useState(-1);
|
| 17 |
+
const [setupResp, setSetupResp] = useState(null);
|
| 18 |
+
const [normalResp, setNormalResp] = useState(null);
|
| 19 |
+
const [suspResp, setSuspResp] = useState(null);
|
| 20 |
+
const [challengeResp, setChallengeResp] = useState(null);
|
| 21 |
+
const [riskData, setRiskData] = useState(null);
|
| 22 |
+
const [challengeId, setChallengeId] = useState('');
|
| 23 |
+
const [challengeCode, setChallengeCode] = useState('');
|
| 24 |
+
const [showChallenge, setShowChallenge] = useState(false);
|
| 25 |
+
const [loading, setLoading] = useState({});
|
| 26 |
+
|
| 27 |
+
const setLoad = (k, v) => setLoading(prev => ({ ...prev, [k]: v }));
|
| 28 |
+
|
| 29 |
+
const setupDemo = async (reset) => {
|
| 30 |
+
setLoad('setup', true);
|
| 31 |
+
const r = await req(`${DEMO}/setup?reset=${reset}`, 'POST', null, false);
|
| 32 |
+
setSetupResp(r);
|
| 33 |
+
if (r.ok) setStep(0);
|
| 34 |
+
setLoad('setup', false);
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
const checkState = async () => {
|
| 38 |
+
setLoad('setup', true);
|
| 39 |
+
const r = await req(`${DEMO}/state`, 'GET', null, false);
|
| 40 |
+
setSetupResp(r);
|
| 41 |
+
setLoad('setup', false);
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
const doNormalLogin = async () => {
|
| 45 |
+
setLoad('normal', true);
|
| 46 |
+
const r = await req(`${DEMO}/scenario1/normal-login`, 'POST', null, false);
|
| 47 |
+
setNormalResp(r);
|
| 48 |
+
if (r.ok && r.data) {
|
| 49 |
+
const fd = r.data.framework_decision || {};
|
| 50 |
+
const t = fd.access_token || r.data.access_token;
|
| 51 |
+
if (t) { saveToken(t); onTokenSave?.(); }
|
| 52 |
+
setRiskData({ decision: fd, notes: r.data.what_the_framework_checked });
|
| 53 |
+
setStep(s => Math.max(s, 1));
|
| 54 |
+
setShowChallenge(false);
|
| 55 |
+
}
|
| 56 |
+
setLoad('normal', false);
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
const doSuspiciousLogin = async () => {
|
| 60 |
+
setLoad('susp', true);
|
| 61 |
+
const r = await req(`${DEMO}/scenario1/suspicious-login`, 'POST', null, false);
|
| 62 |
+
setSuspResp(r);
|
| 63 |
+
if (r.ok && r.data) {
|
| 64 |
+
const fd = r.data.framework_decision || {};
|
| 65 |
+
setRiskData({ decision: fd, notes: r.data.anomalies_triggered });
|
| 66 |
+
setStep(s => Math.max(s, 2));
|
| 67 |
+
if (fd.status === 'challenge_required' && fd.challenge_id) {
|
| 68 |
+
setChallengeId(fd.challenge_id);
|
| 69 |
+
setShowChallenge(true);
|
| 70 |
+
} else {
|
| 71 |
+
setShowChallenge(false);
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
setLoad('susp', false);
|
| 75 |
+
};
|
| 76 |
+
|
| 77 |
+
const doCompleteChallenge = async () => {
|
| 78 |
+
if (!challengeId || !challengeCode) { alert('Enter challenge ID and code.'); return; }
|
| 79 |
+
setLoad('challenge', true);
|
| 80 |
+
const url = `${DEMO}/scenario1/complete-challenge?challenge_id=${encodeURIComponent(challengeId)}&code=${encodeURIComponent(challengeCode)}`;
|
| 81 |
+
const r = await req(url, 'POST', null, false);
|
| 82 |
+
setChallengeResp(r);
|
| 83 |
+
if (r.ok) {
|
| 84 |
+
const t = r.data?.result?.access_token;
|
| 85 |
+
if (t) { saveToken(t); onTokenSave?.(); }
|
| 86 |
+
setStep(3);
|
| 87 |
+
}
|
| 88 |
+
setLoad('challenge', false);
|
| 89 |
+
};
|
| 90 |
+
|
| 91 |
+
return (
|
| 92 |
+
<div>
|
| 93 |
+
<Callout type="info">
|
| 94 |
+
<strong>Scenario 1 — User Behaviour Anomaly Detection</strong><br />
|
| 95 |
+
The demo user has <strong>30 days of normal login history</strong> from New York on Windows Chrome
|
| 96 |
+
(Mon–Fri, 8AM–5PM). We show how the framework reacts when the <em>same password</em> is used
|
| 97 |
+
from a completely different context.
|
| 98 |
+
</Callout>
|
| 99 |
+
|
| 100 |
+
<StepBar steps={STEPS} current={step} />
|
| 101 |
+
|
| 102 |
+
{/* Step 0 — Setup */}
|
| 103 |
+
<Card>
|
| 104 |
+
<CardHeader icon="⚙️">Step 0 – Setup Demo Environment</CardHeader>
|
| 105 |
+
<p className="text-muted text-sm mb-3">
|
| 106 |
+
Creates the demo user with a realistic 30-day behavioral profile (15 logins from a
|
| 107 |
+
trusted IP, device, and time window).
|
| 108 |
+
</p>
|
| 109 |
+
<div className="flex gap-2 flex-wrap">
|
| 110 |
+
<button className="btn btn-primary" onClick={() => setupDemo(false)} disabled={loading.setup}>
|
| 111 |
+
🔧 Setup Demo
|
| 112 |
+
</button>
|
| 113 |
+
<button className="btn btn-warn" onClick={() => setupDemo(true)} disabled={loading.setup}>
|
| 114 |
+
🔄 Reset & Re-setup
|
| 115 |
+
</button>
|
| 116 |
+
<button className="btn btn-ghost" onClick={checkState} disabled={loading.setup}>
|
| 117 |
+
📊 Check State
|
| 118 |
+
</button>
|
| 119 |
+
</div>
|
| 120 |
+
<ResponseBox result={setupResp} />
|
| 121 |
+
</Card>
|
| 122 |
+
|
| 123 |
+
{/* Step 1 & 2 — Compare */}
|
| 124 |
+
<div className="compare-grid">
|
| 125 |
+
{/* Normal */}
|
| 126 |
+
<div className="compare-side success">
|
| 127 |
+
<div className="compare-title success">✅ Normal Context</div>
|
| 128 |
+
<div className="context-list">
|
| 129 |
+
<div><Tag>IP</Tag> 203.0.113.10 (known)</div>
|
| 130 |
+
<div><Tag>Location</Tag> New York, US</div>
|
| 131 |
+
<div><Tag>Device</Tag> Windows Chrome</div>
|
| 132 |
+
<div><Tag>Time</Tag> Business hours</div>
|
| 133 |
+
<div><Tag>History</Tag> 15 logins seen</div>
|
| 134 |
+
</div>
|
| 135 |
+
<button
|
| 136 |
+
className="btn btn-success btn-full mt-3"
|
| 137 |
+
onClick={doNormalLogin}
|
| 138 |
+
disabled={loading.normal}
|
| 139 |
+
>
|
| 140 |
+
{loading.normal ? 'Logging in…' : '▶ Run Normal Login'}
|
| 141 |
+
</button>
|
| 142 |
+
<ResponseBox result={normalResp} />
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
{/* Suspicious */}
|
| 146 |
+
<div className="compare-side danger">
|
| 147 |
+
<div className="compare-title danger">🚩 Suspicious Context</div>
|
| 148 |
+
<div className="context-list">
|
| 149 |
+
<div><Tag>IP</Tag> 198.51.100.55 (new!)</div>
|
| 150 |
+
<div><Tag>Location</Tag> Moscow, Russia</div>
|
| 151 |
+
<div><Tag>Device</Tag> iPhone Safari (new!)</div>
|
| 152 |
+
<div><Tag>Time</Tag> Same password</div>
|
| 153 |
+
<div><Tag>History</Tag> 0 logins from here</div>
|
| 154 |
+
</div>
|
| 155 |
+
<button
|
| 156 |
+
className="btn btn-danger btn-full mt-3"
|
| 157 |
+
onClick={doSuspiciousLogin}
|
| 158 |
+
disabled={loading.susp}
|
| 159 |
+
>
|
| 160 |
+
{loading.susp ? 'Logging in…' : '▶ Run Suspicious Login'}
|
| 161 |
+
</button>
|
| 162 |
+
<ResponseBox result={suspResp} />
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
+
{/* Risk Visualization */}
|
| 167 |
+
{riskData && (
|
| 168 |
+
<RiskVizCard decision={riskData.decision} notes={riskData.notes} />
|
| 169 |
+
)}
|
| 170 |
+
|
| 171 |
+
{/* Step 3 — Challenge */}
|
| 172 |
+
{showChallenge && (
|
| 173 |
+
<Card>
|
| 174 |
+
<CardHeader icon="🔐">Step 3 – Complete Step-up Challenge</CardHeader>
|
| 175 |
+
<Callout type="warn">
|
| 176 |
+
The framework triggered a challenge because of the suspicious context. In a live deployment
|
| 177 |
+
this sends a real email. In the demo, use code <strong>000000</strong>.
|
| 178 |
+
</Callout>
|
| 179 |
+
<div className="grid-2" style={{ alignItems: 'start' }}>
|
| 180 |
+
<div>
|
| 181 |
+
<FormGroup label="Challenge ID">
|
| 182 |
+
<input value={challengeId} readOnly placeholder="Auto-filled from step 2" />
|
| 183 |
+
</FormGroup>
|
| 184 |
+
<FormGroup label="Verification Code">
|
| 185 |
+
<input
|
| 186 |
+
value={challengeCode}
|
| 187 |
+
onChange={e => setChallengeCode(e.target.value)}
|
| 188 |
+
placeholder="Enter code (000000 for demo)"
|
| 189 |
+
/>
|
| 190 |
+
</FormGroup>
|
| 191 |
+
<button
|
| 192 |
+
className="btn btn-primary btn-full"
|
| 193 |
+
onClick={doCompleteChallenge}
|
| 194 |
+
disabled={loading.challenge}
|
| 195 |
+
>
|
| 196 |
+
{loading.challenge ? 'Verifying…' : '✅ Verify & Complete Login'}
|
| 197 |
+
</button>
|
| 198 |
+
</div>
|
| 199 |
+
<div className="text-sm text-2">
|
| 200 |
+
<p className="font-600">Why was this required?</p>
|
| 201 |
+
<ul className="mt-2 ml-4" style={{ lineHeight: 2 }}>
|
| 202 |
+
<li>Unknown IP address</li>
|
| 203 |
+
<li>New device fingerprint</li>
|
| 204 |
+
<li>Geographic location changed</li>
|
| 205 |
+
<li>Security Level ≥ 2 → challenge required</li>
|
| 206 |
+
</ul>
|
| 207 |
+
</div>
|
| 208 |
+
</div>
|
| 209 |
+
<ResponseBox result={challengeResp} />
|
| 210 |
+
</Card>
|
| 211 |
+
)}
|
| 212 |
+
</div>
|
| 213 |
+
);
|
| 214 |
+
}
|
client/src/tabs/Scenario2Tab.jsx
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
| 2 |
+
import { req, saveToken, ENDPOINTS } from '../api';
|
| 3 |
+
import { Card, CardHeader, Callout, ResponseBox, StepBar } from '../components/ui';
|
| 4 |
+
|
| 5 |
+
const DEMO = ENDPOINTS.DEMO;
|
| 6 |
+
const STEPS = [
|
| 7 |
+
{ label: 'Brute Force', sub: 'Inject failed logins' },
|
| 8 |
+
{ label: 'Legit User', sub: 'Normal login during attack' },
|
| 9 |
+
{ label: 'Attacker Unmasked',sub: 'Correct password, still blocked' },
|
| 10 |
+
];
|
| 11 |
+
|
| 12 |
+
function AnomalyFeed({ anomalies }) {
|
| 13 |
+
if (!anomalies || !anomalies.length) {
|
| 14 |
+
return <p className="text-muted text-sm">No active anomalies.</p>;
|
| 15 |
+
}
|
| 16 |
+
return (
|
| 17 |
+
<div>
|
| 18 |
+
{anomalies.map((a, i) => (
|
| 19 |
+
<div className="anomaly-item" key={i}>
|
| 20 |
+
<span style={{ fontSize: 22 }}>🚨</span>
|
| 21 |
+
<div style={{ flex: 1 }}>
|
| 22 |
+
<div className="anomaly-type">{a.type}</div>
|
| 23 |
+
<div className="anomaly-meta">
|
| 24 |
+
IP: <code>{a.ip || '—'}</code> | Confidence: <strong>{a.confidence || '—'}</strong>
|
| 25 |
+
| <span className="badge badge-danger">{a.severity || '—'}</span>
|
| 26 |
+
| {a.first_detected ? new Date(a.first_detected).toLocaleTimeString() : ''}
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
))}
|
| 31 |
+
</div>
|
| 32 |
+
);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function CompareBox({ legitResult, attackerResult }) {
|
| 36 |
+
if (!legitResult && !attackerResult) {
|
| 37 |
+
return <p className="text-muted text-sm">Run steps 2 and 3 to see the comparison.</p>;
|
| 38 |
+
}
|
| 39 |
+
const ld = (legitResult?.framework_decision) || {};
|
| 40 |
+
const ad = (attackerResult?.framework_decision) || {};
|
| 41 |
+
const col = s => s === 'success' ? 'var(--success)' : s === 'challenge_required' ? 'var(--warn)' : 'var(--danger)';
|
| 42 |
+
const icon = s => s === 'success' ? '✅' : s === 'challenge_required' ? '⚠️' : '🚫';
|
| 43 |
+
return (
|
| 44 |
+
<div className="side-compare">
|
| 45 |
+
<div className="side-item">
|
| 46 |
+
<div className="side-title text-success">Legitimate User</div>
|
| 47 |
+
{legitResult ? (
|
| 48 |
+
<>
|
| 49 |
+
<div style={{ fontSize: 26, textAlign: 'center', margin: '8px 0', color: col(ld.status) }}>{icon(ld.status)}</div>
|
| 50 |
+
<div style={{ textAlign: 'center', fontWeight: 700, color: col(ld.status) }}>{(ld.status || '').toUpperCase()}</div>
|
| 51 |
+
<div className="text-sm text-muted mt-1">Risk: {ld.risk_level || '—'} | Level: {ld.security_level ?? '—'}</div>
|
| 52 |
+
{legitResult.key_insight && <div className="text-sm mt-1">{legitResult.key_insight}</div>}
|
| 53 |
+
</>
|
| 54 |
+
) : <p className="text-muted text-sm">Run step 2</p>}
|
| 55 |
+
</div>
|
| 56 |
+
<div className="side-item">
|
| 57 |
+
<div className="side-title text-danger">Attacker (correct password)</div>
|
| 58 |
+
{attackerResult ? (
|
| 59 |
+
<>
|
| 60 |
+
<div style={{ fontSize: 26, textAlign: 'center', margin: '8px 0', color: col(ad.status) }}>{icon(ad.status)}</div>
|
| 61 |
+
<div style={{ textAlign: 'center', fontWeight: 700, color: col(ad.status) }}>{(ad.status || '').toUpperCase()}</div>
|
| 62 |
+
<div className="text-sm text-muted mt-1">Risk: {ad.risk_level || '—'} | Level: {ad.security_level ?? '—'}</div>
|
| 63 |
+
{attackerResult.key_insight && <div className="text-sm mt-1">{attackerResult.key_insight}</div>}
|
| 64 |
+
</>
|
| 65 |
+
) : <p className="text-muted text-sm">Run step 3</p>}
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
export default function Scenario2Tab({ onTokenSave }) {
|
| 72 |
+
const [step, setStep] = useState(-1);
|
| 73 |
+
const [attackAttempts, setAtk] = useState('12');
|
| 74 |
+
const [attackLog, setAtkLog] = useState([]);
|
| 75 |
+
const [attackResp, setAtkResp] = useState(null);
|
| 76 |
+
const [legitResp, setLegitResp] = useState(null);
|
| 77 |
+
const [attackerResp, setAtkrResp] = useState(null);
|
| 78 |
+
const [legitResult, setLegitResult] = useState(null);
|
| 79 |
+
const [attackerResult, setAtkrResult] = useState(null);
|
| 80 |
+
const [anomalies, setAnomalies] = useState([]);
|
| 81 |
+
const [isMonitoring, setMonitor] = useState(false);
|
| 82 |
+
const [monStats, setMonStats] = useState(null);
|
| 83 |
+
const [monLog, setMonLog] = useState([]);
|
| 84 |
+
const [loading, setLoading] = useState({});
|
| 85 |
+
|
| 86 |
+
const intervalRef = useRef(null);
|
| 87 |
+
const cycleCountRef = useRef(0);
|
| 88 |
+
const runCycleRef = useRef(null);
|
| 89 |
+
|
| 90 |
+
const setLoad = (k, v) => setLoading(p => ({ ...p, [k]: v }));
|
| 91 |
+
|
| 92 |
+
const addLog = useCallback((type, msg) => {
|
| 93 |
+
setMonLog(prev => {
|
| 94 |
+
const next = [...prev, { type, msg, id: Date.now() + Math.random() }];
|
| 95 |
+
return next.length > 200 ? next.slice(-200) : next;
|
| 96 |
+
});
|
| 97 |
+
}, []);
|
| 98 |
+
|
| 99 |
+
const refreshAnomalies = useCallback(async () => {
|
| 100 |
+
const r = await req(`${DEMO}/scenario2/anomalies`, 'GET', null, false);
|
| 101 |
+
if (r.ok && r.data?.active_anomalies?.length) {
|
| 102 |
+
setAnomalies(r.data.active_anomalies);
|
| 103 |
+
} else {
|
| 104 |
+
setAnomalies([]);
|
| 105 |
+
}
|
| 106 |
+
}, []);
|
| 107 |
+
|
| 108 |
+
const runCycle = useCallback(async () => {
|
| 109 |
+
cycleCountRef.current++;
|
| 110 |
+
const r = await req(`${DEMO}/scenario2/run-monitoring-cycle`, 'POST', null, false);
|
| 111 |
+
if (!r.ok) {
|
| 112 |
+
addLog('warn', `Cycle ${cycleCountRef.current} failed: ${r.data?.detail || 'server error'}`);
|
| 113 |
+
return;
|
| 114 |
+
}
|
| 115 |
+
const d = r.data;
|
| 116 |
+
const threat = d.threat_level || 'NORMAL';
|
| 117 |
+
const anom = d.scan?.total_active_anomalies ?? 0;
|
| 118 |
+
const time = new Date(d.cycle_at).toLocaleTimeString();
|
| 119 |
+
const bf = d.scan?.brute_force_active ? ' | BruteForce:ACTIVE' : '';
|
| 120 |
+
const cs = d.scan?.credential_stuffing_active ? ' | CredStuffing:ACTIVE' : '';
|
| 121 |
+
setMonStats(d);
|
| 122 |
+
addLog(anom > 0 ? 'threat' : 'ok',
|
| 123 |
+
`[${time}] #${cycleCountRef.current} ${anom > 0 ? '⚠' : '✓'} Threat:${threat} Anomalies:${anom} Failed/1h:${d.scan?.recent_failed_logins_1h ?? 0}${bf}${cs}`
|
| 124 |
+
);
|
| 125 |
+
if (cycleCountRef.current % 3 === 0) await refreshAnomalies();
|
| 126 |
+
}, [addLog, refreshAnomalies]);
|
| 127 |
+
|
| 128 |
+
// Keep ref current so interval always calls the latest version
|
| 129 |
+
runCycleRef.current = runCycle;
|
| 130 |
+
|
| 131 |
+
const startMonitoring = () => {
|
| 132 |
+
cycleCountRef.current = 0;
|
| 133 |
+
setMonitor(true);
|
| 134 |
+
addLog('info', 'Continuous monitoring started. Scanning every 2 s…');
|
| 135 |
+
runCycleRef.current();
|
| 136 |
+
intervalRef.current = setInterval(() => runCycleRef.current(), 2000);
|
| 137 |
+
};
|
| 138 |
+
|
| 139 |
+
const stopMonitoring = () => {
|
| 140 |
+
if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; }
|
| 141 |
+
setMonitor(false);
|
| 142 |
+
addLog('info', `Monitoring stopped after ${cycleCountRef.current} cycles.`);
|
| 143 |
+
};
|
| 144 |
+
|
| 145 |
+
useEffect(() => () => {
|
| 146 |
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
| 147 |
+
}, []);
|
| 148 |
+
|
| 149 |
+
const doSimulateAttack = async () => {
|
| 150 |
+
setLoad('attack', true);
|
| 151 |
+
const n = parseInt(attackAttempts) || 12;
|
| 152 |
+
const log = [];
|
| 153 |
+
for (let i = 1; i <= n; i++) {
|
| 154 |
+
await new Promise(r => setTimeout(r, 70));
|
| 155 |
+
log.push({ id: i, msg: `[${new Date().toLocaleTimeString()}] ATTEMPT ${i}/${n} pw:****${i} → FAILED`, type: 'attempt' });
|
| 156 |
+
setAtkLog([...log]);
|
| 157 |
+
}
|
| 158 |
+
const r = await req(`${DEMO}/scenario2/simulate-attack?num_attempts=${n}`, 'POST', null, false);
|
| 159 |
+
setAtkResp(r);
|
| 160 |
+
if (r.ok && r.data) {
|
| 161 |
+
const cnt = r.data.anomalies_detected?.length || 0;
|
| 162 |
+
const aip = r.data.attack_details?.attacker_ip || '192.0.2.100';
|
| 163 |
+
setAtkLog(prev => [
|
| 164 |
+
...prev,
|
| 165 |
+
{ id: 'det', msg: `[FRAMEWORK] AnomalyDetector fired: ${cnt} pattern(s) detected.`, type: 'detected' },
|
| 166 |
+
{ id: 'blk', msg: `[FRAMEWORK] IP ${aip} flagged as CRITICAL. All requests BLOCKED.`, type: 'blocked' },
|
| 167 |
+
]);
|
| 168 |
+
setStep(s => Math.max(s, 0));
|
| 169 |
+
}
|
| 170 |
+
await refreshAnomalies();
|
| 171 |
+
setLoad('attack', false);
|
| 172 |
+
};
|
| 173 |
+
|
| 174 |
+
const doLegitLogin = async () => {
|
| 175 |
+
setLoad('legit', true);
|
| 176 |
+
const r = await req(`${DEMO}/scenario2/legitimate-user`, 'POST', null, false);
|
| 177 |
+
setLegitResp(r);
|
| 178 |
+
setLegitResult(r.data);
|
| 179 |
+
if (r.ok) {
|
| 180 |
+
const t = r.data?.framework_decision?.access_token || r.data?.access_token;
|
| 181 |
+
if (t) { saveToken(t); onTokenSave?.(); }
|
| 182 |
+
setStep(s => Math.max(s, 1));
|
| 183 |
+
}
|
| 184 |
+
setLoad('legit', false);
|
| 185 |
+
};
|
| 186 |
+
|
| 187 |
+
const doAttackerLogin = async () => {
|
| 188 |
+
setLoad('attacker', true);
|
| 189 |
+
const r = await req(`${DEMO}/scenario2/attacker-login-attempt`, 'POST', null, false);
|
| 190 |
+
setAtkrResp(r);
|
| 191 |
+
setAtkrResult(r.data);
|
| 192 |
+
if (r.ok) setStep(s => Math.max(s, 2));
|
| 193 |
+
setLoad('attacker', false);
|
| 194 |
+
};
|
| 195 |
+
|
| 196 |
+
const clearAnomalies = async () => {
|
| 197 |
+
await req(`${DEMO}/scenario2/clear-anomalies`, 'DELETE', null, false);
|
| 198 |
+
setAnomalies([]);
|
| 199 |
+
addLog('info', 'All anomalies cleared.');
|
| 200 |
+
};
|
| 201 |
+
|
| 202 |
+
const logClass = { attempt: 'attack-line', detected: 'attack-detected', blocked: 'attack-blocked' };
|
| 203 |
+
|
| 204 |
+
return (
|
| 205 |
+
<div>
|
| 206 |
+
<Callout type="danger">
|
| 207 |
+
<strong>Scenario 2 — Attack & Anomaly Detection</strong><br />
|
| 208 |
+
We simulate a brute-force attack, watch the <code>AnomalyDetector</code> fire in real time,
|
| 209 |
+
then compare a <em>legitimate user</em> vs the <em>attacker with the correct password</em>.
|
| 210 |
+
</Callout>
|
| 211 |
+
|
| 212 |
+
<StepBar steps={STEPS} current={step} />
|
| 213 |
+
|
| 214 |
+
<div className="grid-2" style={{ alignItems: 'start' }}>
|
| 215 |
+
{/* Left column */}
|
| 216 |
+
<div>
|
| 217 |
+
{/* Attack Simulation */}
|
| 218 |
+
<Card>
|
| 219 |
+
<CardHeader icon="💣">Step 1 – Simulate Brute Force Attack</CardHeader>
|
| 220 |
+
<p className="text-sm text-muted mb-3">
|
| 221 |
+
Injects failed login attempts from attacker IP <code>192.0.2.100</code> (Beijing, China),
|
| 222 |
+
then triggers the AnomalyDetector.
|
| 223 |
+
</p>
|
| 224 |
+
<div className="form-group">
|
| 225 |
+
<label>Number of attempts</label>
|
| 226 |
+
<input
|
| 227 |
+
type="number"
|
| 228 |
+
value={attackAttempts}
|
| 229 |
+
onChange={e => setAtk(e.target.value)}
|
| 230 |
+
min="5" max="25"
|
| 231 |
+
/>
|
| 232 |
+
</div>
|
| 233 |
+
<button
|
| 234 |
+
className="btn btn-danger btn-full"
|
| 235 |
+
onClick={doSimulateAttack}
|
| 236 |
+
disabled={loading.attack}
|
| 237 |
+
>
|
| 238 |
+
{loading.attack ? 'Simulating…' : '💥 Launch Attack Simulation'}
|
| 239 |
+
</button>
|
| 240 |
+
{attackLog.length > 0 && (
|
| 241 |
+
<div className="attack-log">
|
| 242 |
+
{attackLog.map(l => (
|
| 243 |
+
<div key={l.id} className={logClass[l.type] || 'attack-line'}>{l.msg}</div>
|
| 244 |
+
))}
|
| 245 |
+
</div>
|
| 246 |
+
)}
|
| 247 |
+
<ResponseBox result={attackResp} />
|
| 248 |
+
</Card>
|
| 249 |
+
|
| 250 |
+
{/* Monitoring */}
|
| 251 |
+
<Card>
|
| 252 |
+
<CardHeader
|
| 253 |
+
icon="🚨"
|
| 254 |
+
actions={
|
| 255 |
+
<span className={`monitor-badge ${isMonitoring ? 'mon-on' : 'mon-off'}`}>
|
| 256 |
+
{isMonitoring ? '● Live' : '⏸ Idle'}
|
| 257 |
+
</span>
|
| 258 |
+
}
|
| 259 |
+
>
|
| 260 |
+
Live Anomaly Feed & Monitoring
|
| 261 |
+
</CardHeader>
|
| 262 |
+
|
| 263 |
+
<div className="flex gap-2 flex-wrap mb-3 items-center">
|
| 264 |
+
<button
|
| 265 |
+
className={`btn btn-sm ${isMonitoring ? 'btn-danger' : 'btn-success'}`}
|
| 266 |
+
onClick={isMonitoring ? stopMonitoring : startMonitoring}
|
| 267 |
+
>
|
| 268 |
+
{isMonitoring ? '⏹ Stop Monitoring' : '▶ Start Monitoring'}
|
| 269 |
+
</button>
|
| 270 |
+
<button className="btn btn-ghost btn-sm" onClick={refreshAnomalies}>🔄 Refresh Feed</button>
|
| 271 |
+
<button className="btn btn-danger btn-sm" onClick={clearAnomalies}>🗑 Clear</button>
|
| 272 |
+
<span className="text-xs text-muted">Runs a full scan cycle every 2 s when active</span>
|
| 273 |
+
</div>
|
| 274 |
+
|
| 275 |
+
{isMonitoring && monStats && (
|
| 276 |
+
<div className="monitor-stats">
|
| 277 |
+
{[
|
| 278 |
+
{ val: monStats.threat_level || 'NORMAL', lbl: 'Threat', color: monStats.threat_level === 'NORMAL' ? 'var(--success)' : 'var(--danger)' },
|
| 279 |
+
{ val: monStats.scan?.total_active_anomalies ?? '—', lbl: 'Anomalies', color: 'var(--warn)' },
|
| 280 |
+
{ val: monStats.scan?.recent_failed_logins_1h ?? '—', lbl: 'Failed/1h', color: 'var(--danger)' },
|
| 281 |
+
{ val: monStats.sessions?.active ?? '—', lbl: 'Sessions', color: 'var(--info)' },
|
| 282 |
+
{ val: monStats.sessions?.suspicious ?? '—', lbl: 'Suspicious', color: 'var(--warn)' },
|
| 283 |
+
].map(s => (
|
| 284 |
+
<div className="ms-item" key={s.lbl}>
|
| 285 |
+
<div className="ms-val" style={{ color: s.color }}>{s.val}</div>
|
| 286 |
+
<div className="ms-lbl">{s.lbl}</div>
|
| 287 |
+
</div>
|
| 288 |
+
))}
|
| 289 |
+
<div className="ms-item ml-auto">
|
| 290 |
+
<div className="ms-val" style={{ fontSize: 11, color: 'var(--muted)' }}>
|
| 291 |
+
{monStats.cycle_at ? new Date(monStats.cycle_at).toLocaleTimeString() : '—'}
|
| 292 |
+
</div>
|
| 293 |
+
<div className="ms-lbl">Last cycle</div>
|
| 294 |
+
</div>
|
| 295 |
+
</div>
|
| 296 |
+
)}
|
| 297 |
+
|
| 298 |
+
{monLog.length > 0 && (
|
| 299 |
+
<div className="monitor-log">
|
| 300 |
+
{monLog.map(l => (
|
| 301 |
+
<div key={l.id} className={`ml-${l.type}`}>{l.msg}</div>
|
| 302 |
+
))}
|
| 303 |
+
</div>
|
| 304 |
+
)}
|
| 305 |
+
|
| 306 |
+
<div className="mt-3">
|
| 307 |
+
<AnomalyFeed anomalies={anomalies} />
|
| 308 |
+
</div>
|
| 309 |
+
</Card>
|
| 310 |
+
</div>
|
| 311 |
+
|
| 312 |
+
{/* Right column */}
|
| 313 |
+
<div>
|
| 314 |
+
<Card>
|
| 315 |
+
<CardHeader icon="👤">Step 2 – Legitimate User Logs In</CardHeader>
|
| 316 |
+
<p className="text-sm text-muted mb-3">
|
| 317 |
+
Same account. Logs in from <strong>New York (trusted IP, trusted device)</strong> while
|
| 318 |
+
the attack is in progress.
|
| 319 |
+
</p>
|
| 320 |
+
<button className="btn btn-success btn-full" onClick={doLegitLogin} disabled={loading.legit}>
|
| 321 |
+
{loading.legit ? 'Logging in…' : '▶ Login as Legitimate User'}
|
| 322 |
+
</button>
|
| 323 |
+
<ResponseBox result={legitResp} />
|
| 324 |
+
</Card>
|
| 325 |
+
|
| 326 |
+
<Card>
|
| 327 |
+
<CardHeader icon="🤖">Step 3 – Attacker Uses Correct Password</CardHeader>
|
| 328 |
+
<p className="text-sm text-muted mb-3">
|
| 329 |
+
The attacker somehow obtained the real password. See what happens.<br />
|
| 330 |
+
<strong>Spoiler:</strong> correct password alone is not enough.
|
| 331 |
+
</p>
|
| 332 |
+
<button className="btn btn-warn btn-full" onClick={doAttackerLogin} disabled={loading.attacker}>
|
| 333 |
+
{loading.attacker ? 'Attempting…' : '🔑 Attacker Login Attempt'}
|
| 334 |
+
</button>
|
| 335 |
+
<ResponseBox result={attackerResp} />
|
| 336 |
+
</Card>
|
| 337 |
+
|
| 338 |
+
<Card>
|
| 339 |
+
<CardHeader icon="⚖️">Side-by-side Comparison</CardHeader>
|
| 340 |
+
<CompareBox legitResult={legitResult} attackerResult={attackerResult} />
|
| 341 |
+
</Card>
|
| 342 |
+
</div>
|
| 343 |
+
</div>
|
| 344 |
+
</div>
|
| 345 |
+
);
|
| 346 |
+
}
|
client/vite.config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
// https://vite.dev/config/
|
| 5 |
+
export default defineConfig(({ mode }) => ({
|
| 6 |
+
plugins: [react()],
|
| 7 |
+
// In production, assets are served from FastAPI's /static mount.
|
| 8 |
+
// In dev, Vite dev-server proxies API calls and serves everything from root.
|
| 9 |
+
base: mode === 'production' ? '/static/' : '/',
|
| 10 |
+
build: {
|
| 11 |
+
outDir: '../static', // demo-auth/static/
|
| 12 |
+
emptyOutDir: true,
|
| 13 |
+
},
|
| 14 |
+
server: {
|
| 15 |
+
port: 5173,
|
| 16 |
+
proxy: {
|
| 17 |
+
'/api': { target: 'http://localhost:8000', changeOrigin: true },
|
| 18 |
+
'/health': { target: 'http://localhost:8000', changeOrigin: true },
|
| 19 |
+
},
|
| 20 |
+
},
|
| 21 |
+
}))
|
static/assets/index-C4kGMRC-.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/assets/index-DKV7YaMs.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}:root{--bg: #f0f4f8;--surface: #ffffff;--surface-2: #f8fafc;--border: #e2e8f0;--border-2: #cbd5e1;--text: #0f172a;--text-2: #334155;--muted: #64748b;--placeholder: #94a3b8;--primary: #2563eb;--primary-h: #1d4ed8;--primary-50: #eff6ff;--primary-100: #dbeafe;--primary-text: #1e40af;--success: #16a34a;--success-h: #15803d;--success-50: #f0fdf4;--success-100: #dcfce7;--success-border: #86efac;--warn: #d97706;--warn-h: #b45309;--warn-50: #fffbeb;--warn-100: #fef3c7;--warn-border: #fcd34d;--danger: #dc2626;--danger-h: #b91c1c;--danger-50: #fef2f2;--danger-100: #fee2e2;--danger-border: #fca5a5;--info: #0284c7;--info-50: #f0f9ff;--info-100: #e0f2fe;--info-border: #7dd3fc;--r: 8px;--r-sm: 5px;--r-lg: 12px;--shadow-sm: 0 1px 2px rgba(0,0,0,.05);--shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.05);--shadow-md: 0 4px 6px rgba(0,0,0,.06), 0 2px 4px rgba(0,0,0,.05)}body{font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;font-size:14px;-webkit-font-smoothing:antialiased}.topbar{background:var(--surface);border-bottom:1px solid var(--border);height:56px;padding:0 28px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;box-shadow:var(--shadow-sm)}.topbar-logo{font-size:18px;font-weight:800;color:var(--primary);letter-spacing:-.3px;display:flex;align-items:center;gap:7px}.topbar-logo em{color:var(--text);font-style:normal;font-weight:600}.topbar-status{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}.status-dot{width:8px;height:8px;border-radius:50%;background:var(--danger);flex-shrink:0;transition:background .3s}.status-dot.online{background:var(--success)}.status-dot.checking{background:var(--warn);animation:pulse 1s ease-in-out infinite}.container{max-width:1280px;margin:0 auto;padding:28px 28px 60px}.hero{padding-bottom:20px;margin-bottom:24px;border-bottom:1px solid var(--border)}.hero h1{font-size:24px;font-weight:800;color:var(--text);letter-spacing:-.4px;margin-bottom:4px}.hero h1 span{color:var(--primary)}.hero p{color:var(--muted);font-size:13px}.token-bar{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:10px 16px;margin-bottom:20px;display:flex;align-items:center;gap:10px;box-shadow:var(--shadow-sm)}.token-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--muted);white-space:nowrap}.token-val{flex:1;font-family:Consolas,Cascadia Code,monospace;font-size:12px;color:var(--primary-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.main-tabs{display:flex;gap:0;border-bottom:2px solid var(--border);margin-bottom:24px;overflow-x:auto}.main-tabs button{background:none;border:none;border-bottom:2px solid transparent;margin-bottom:-2px;color:var(--muted);padding:10px 20px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;transition:color .15s,border-color .15s;font-family:inherit}.main-tabs button.active{color:var(--primary);border-bottom-color:var(--primary)}.main-tabs button:hover:not(.active){color:var(--text-2)}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:20px 22px;margin-bottom:16px;box-shadow:var(--shadow)}.card-header{display:flex;align-items:center;gap:8px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);padding-bottom:12px;margin-bottom:14px;border-bottom:1px solid var(--border)}.card-header-row{display:flex;align-items:center;justify-content:space-between;padding-bottom:12px;margin-bottom:14px;border-bottom:1px solid var(--border)}.card-header-row .card-title{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);display:flex;align-items:center;gap:8px}button,.btn{display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:8px 16px;border:1px solid transparent;border-radius:var(--r-sm);font-size:13px;font-weight:600;cursor:pointer;transition:background .12s,filter .1s,transform .08s,box-shadow .12s;white-space:nowrap;font-family:inherit;text-align:center}button:active:not(:disabled){transform:scale(.97)}button:disabled{opacity:.45;cursor:not-allowed}.btn-primary{background:var(--primary);color:#fff;border-color:var(--primary-h)}.btn-primary:hover:not(:disabled){background:var(--primary-h)}.btn-success{background:var(--success);color:#fff;border-color:var(--success-h)}.btn-success:hover:not(:disabled){background:var(--success-h)}.btn-warn{background:var(--warn);color:#fff;border-color:var(--warn-h)}.btn-warn:hover:not(:disabled){background:var(--warn-h)}.btn-danger{background:var(--danger);color:#fff;border-color:var(--danger-h)}.btn-danger:hover:not(:disabled){background:var(--danger-h)}.btn-ghost{background:var(--surface);color:var(--text-2);border-color:var(--border);box-shadow:var(--shadow-sm)}.btn-ghost:hover:not(:disabled){background:var(--bg);border-color:var(--border-2)}.btn-sm{padding:5px 11px;font-size:12px}.btn-full{width:100%}.form-group{margin-bottom:13px}.form-group label{display:block;font-size:11px;font-weight:600;color:var(--muted);margin-bottom:5px;text-transform:uppercase;letter-spacing:.5px}input,select,.form-input{width:100%;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-sm);color:var(--text);padding:8px 11px;font-size:13px;font-family:inherit;transition:border-color .15s,box-shadow .15s}input:focus,select:focus,.form-input:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-100)}input::placeholder{color:var(--placeholder)}input[readonly]{background:var(--surface-2);color:var(--muted);cursor:default}.resp-box{margin-top:10px;padding:12px 14px;border-radius:var(--r-sm);font-size:12px;font-family:Consolas,Cascadia Code,monospace;background:var(--surface-2);border:1px solid var(--border);white-space:pre-wrap;word-break:break-all;max-height:280px;overflow-y:auto;line-height:1.6;color:var(--text-2)}.resp-box.ok{border-color:var(--success-border);color:var(--success);background:var(--success-50)}.resp-box.err{border-color:var(--danger-border);color:var(--danger);background:var(--danger-50)}.callout{padding:12px 15px;border-radius:var(--r-sm);font-size:13px;line-height:1.65;margin-bottom:14px;border-left:3px solid}.callout-info{background:var(--info-50);border-color:var(--info);color:#0c4a6e}.callout-success{background:var(--success-50);border-color:var(--success);color:#14532d}.callout-warn{background:var(--warn-50);border-color:var(--warn);color:#78350f}.callout-danger{background:var(--danger-50);border-color:var(--danger);color:#7f1d1d}.badge{display:inline-flex;align-items:center;padding:2px 9px;border-radius:4px;font-size:11px;font-weight:700;letter-spacing:.4px;text-transform:uppercase}.badge-success{background:var(--success-100);color:var(--success);border:1px solid var(--success-border)}.badge-warn{background:var(--warn-100);color:var(--warn);border:1px solid var(--warn-border)}.badge-danger{background:var(--danger-100);color:var(--danger);border:1px solid var(--danger-border)}.badge-info{background:var(--info-100);color:var(--info);border:1px solid var(--info-border)}.badge-muted{background:var(--surface-2);color:var(--muted);border:1px solid var(--border)}.tag{display:inline-block;padding:1px 7px;border-radius:4px;font-size:11px;background:var(--primary-50);color:var(--primary-text);margin:2px;font-weight:600;border:1px solid var(--primary-100)}.pill{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border-radius:4px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.3px}.pill-ok{background:var(--success-100);color:var(--success);border:1px solid var(--success-border)}.pill-err{background:var(--danger-100);color:var(--danger);border:1px solid var(--danger-border)}.pill-warn{background:var(--warn-100);color:var(--warn);border:1px solid var(--warn-border)}.step-bar{display:flex;gap:8px;margin-bottom:20px;overflow-x:auto}.step-item{flex:1;min-width:120px;background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:11px 14px;text-align:center;box-shadow:var(--shadow-sm);transition:border-color .2s}.step-item.active{border-color:var(--primary);background:var(--primary-50)}.step-item.done{border-color:var(--success-border);background:var(--success-50)}.step-num{width:26px;height:26px;border-radius:50%;background:var(--border);color:var(--muted);display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;margin-bottom:5px}.step-item.active .step-num{background:var(--primary);color:#fff}.step-item.done .step-num{background:var(--success);color:#fff;font-size:14px}.step-label{font-size:12px;font-weight:700;color:var(--text-2)}.step-sub{font-size:11px;color:var(--muted);margin-top:2px}.compare-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px}.compare-side{background:var(--surface);border-radius:var(--r);padding:16px;border:1px solid var(--border);box-shadow:var(--shadow-sm)}.compare-side.success{border-top:3px solid var(--success)}.compare-side.danger{border-top:3px solid var(--danger)}.compare-title{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px}.compare-title.success{color:var(--success)}.compare-title.danger{color:var(--danger)}.context-list{display:flex;flex-direction:column;gap:5px;font-size:13px;margin-bottom:10px}.context-list>div{color:var(--text-2)}.gauge-wrap{text-align:center;padding:10px 0 4px}.gauge-val{font-size:32px;font-weight:800;letter-spacing:-1px;line-height:1;margin-top:2px}.gauge-label{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--muted);margin-top:4px}.level-bar{display:flex;gap:4px;margin-top:8px}.level-seg{flex:1;height:7px;border-radius:4px;background:var(--border);transition:background .4s}.level-seg.seg-0{background:var(--success)}.level-seg.seg-1{background:#84cc16}.level-seg.seg-2{background:var(--warn)}.level-seg.seg-3{background:#f97316}.level-seg.seg-4{background:var(--danger)}.factor-row{margin-bottom:8px}.factor-label{font-size:12px;color:var(--muted);margin-bottom:4px;display:flex;justify-content:space-between}.factor-bar-wrap{height:8px;background:var(--border);border-radius:4px;overflow:hidden}.factor-bar{height:100%;border-radius:4px;transition:width .7s ease}.decision-panel{border-radius:var(--r-sm);overflow:hidden;border:1px solid var(--border);margin-top:8px}.decision-header{padding:11px 14px;font-size:14px;font-weight:800;text-align:center;letter-spacing:.3px}.decision-header.success{background:var(--success-50);color:var(--success);border-bottom:1px solid var(--success-border)}.decision-header.challenge{background:var(--warn-50);color:var(--warn);border-bottom:1px solid var(--warn-border)}.decision-header.blocked{background:var(--danger-50);color:var(--danger);border-bottom:1px solid var(--danger-border)}.decision-body{padding:12px 14px;font-size:13px;background:var(--surface);color:var(--text-2)}.attack-log{background:#1e1b4b;border-radius:var(--r-sm);padding:10px 13px;font-family:Consolas,monospace;font-size:12px;height:190px;overflow-y:auto;margin-top:12px}.attack-line{margin:2px 0;color:#c7d2fe}.attack-detected{color:#fcd34d;font-weight:700}.attack-blocked{color:#f9a8d4;font-weight:700}.anomaly-item{background:var(--danger-50);border:1px solid var(--danger-border);border-radius:var(--r-sm);padding:10px 13px;margin-bottom:8px;display:flex;align-items:flex-start;gap:10px;animation:slideIn .25s ease}.anomaly-type{font-weight:700;font-size:13px;color:var(--danger)}.anomaly-meta{font-size:11px;color:var(--muted);margin-top:3px}.monitor-badge{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:4px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;transition:all .2s}.monitor-badge.mon-on{background:var(--danger-100);color:var(--danger);border:1px solid var(--danger-border);animation:pulse 1.4s ease-in-out infinite}.monitor-badge.mon-off{background:var(--surface-2);color:var(--muted);border:1px solid var(--border)}.monitor-stats{display:flex;flex-wrap:wrap;gap:8px;padding:10px 14px;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--r-sm);font-size:12px;margin-bottom:10px}.ms-item{display:flex;flex-direction:column;align-items:center;min-width:56px}.ms-val{font-size:20px;font-weight:800;line-height:1;letter-spacing:-.3px}.ms-lbl{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;margin-top:2px}.monitor-log{background:#0f172a;border-radius:var(--r-sm);padding:10px 13px;font-family:Consolas,monospace;font-size:11.5px;height:140px;overflow-y:auto;line-height:1.7;margin-top:10px}.ml-ok{color:#86efac}.ml-threat{color:#fca5a5;font-weight:700}.ml-warn{color:#fcd34d}.ml-info{color:#93c5fd}.stat-box{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:18px 12px;text-align:center;box-shadow:var(--shadow-sm)}.stat-num{font-size:32px;font-weight:800;line-height:1;letter-spacing:-.5px}.stat-label{font-size:10px;color:var(--muted);margin-top:6px;text-transform:uppercase;letter-spacing:.6px}.trust-label{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.6px;margin-top:4px;text-align:center}.behavior-bar-group{margin-bottom:10px}.behavior-bar-header{display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px;color:var(--text-2)}.behavior-bar-track{height:8px;background:var(--border);border-radius:4px;overflow:hidden}.behavior-bar-fill{height:100%;border-radius:4px;transition:width .5s,background .5s}code{background:var(--primary-50);border:1px solid var(--primary-100);border-radius:3px;padding:1px 5px;font-size:11.5px;color:var(--primary-text);font-family:Consolas,monospace}.qr-img{max-width:180px;border:3px solid var(--border);border-radius:var(--r);display:block;margin:10px auto}.side-compare{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:10px}.side-item{border:1px solid var(--border);border-radius:var(--r-sm);padding:14px;background:var(--surface)}.side-title{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px}.env-row{display:flex;justify-content:space-between;align-items:center;padding:5px 0;border-bottom:1px solid var(--border);font-size:12px}.env-key{font-family:Consolas,monospace;color:var(--text-2)}.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:16px}.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.flex{display:flex}.flex-wrap{flex-wrap:wrap}.flex-col{flex-direction:column}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:4px}.gap-2{gap:8px}.gap-3{gap:12px}.gap-4{gap:16px}.flex-1{flex:1}.min-w-0{min-width:0}.mt-1{margin-top:4px}.mt-2{margin-top:8px}.mt-3{margin-top:12px}.mt-4{margin-top:16px}.mb-1{margin-bottom:4px}.mb-2{margin-bottom:8px}.mb-3{margin-bottom:12px}.mb-4{margin-bottom:16px}.ml-auto{margin-left:auto}.ml-4{margin-left:16px}.p-3{padding:12px}.p-4{padding:16px}.text-sm{font-size:12px}.text-xs{font-size:11px}.text-muted{color:var(--muted)}.text-2{color:var(--text-2)}.text-primary{color:var(--primary)}.text-success{color:var(--success)}.text-warn{color:var(--warn)}.text-danger{color:var(--danger)}.text-center{text-align:center}.font-bold{font-weight:700}.font-600{font-weight:600}.font-800{font-weight:800}.w-full{width:100%}.uppercase{text-transform:uppercase}.letter-wide{letter-spacing:.5px}.mono{font-family:Consolas,monospace}.border-t{border-top:1px solid var(--border);padding-top:12px;margin-top:12px}@keyframes slideIn{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.35}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:var(--surface-2)}::-webkit-scrollbar-thumb{background:var(--border-2);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--muted)}@media(max-width:900px){.grid-4,.grid-3{grid-template-columns:1fr 1fr}.compare-grid,.side-compare{grid-template-columns:1fr}}@media(max-width:600px){.grid-2,.grid-3,.grid-4{grid-template-columns:1fr}.container{padding:16px}.topbar{padding:0 16px}}
|
static/index.html
CHANGED
|
@@ -1,1570 +1,19 @@
|
|
| 1 |
-
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>AdaptiveAuth
|
| 7 |
-
<
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
/* Topbar — flat, no gradient */
|
| 22 |
-
.topbar{background:var(--c-surface);border-bottom:1px solid var(--c-border);padding:0 28px;display:flex;align-items:center;justify-content:space-between;height:52px;position:sticky;top:0;z-index:100}
|
| 23 |
-
.topbar-logo{font-size:17px;font-weight:700;color:var(--c-primary);letter-spacing:-.2px}
|
| 24 |
-
.topbar-logo span{color:var(--c-text);font-weight:400}
|
| 25 |
-
.topbar-status{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--c-muted)}
|
| 26 |
-
.dot{width:7px;height:7px;border-radius:50%;background:var(--c-danger)}.dot.online{background:var(--c-success)}
|
| 27 |
-
|
| 28 |
-
/* Layout */
|
| 29 |
-
.container{max-width:1260px;margin:0 auto;padding:24px 28px}
|
| 30 |
-
.main-tabs{display:flex;gap:0;border-bottom:1px solid var(--c-border);margin-bottom:24px;overflow-x:auto}
|
| 31 |
-
.main-tabs button{background:none;border:none;border-bottom:2px solid transparent;color:var(--c-muted);padding:10px 20px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;transition:color .15s,border-color .15s}
|
| 32 |
-
.main-tabs button.active{color:var(--c-primary);border-bottom-color:var(--c-primary)}
|
| 33 |
-
.main-tabs button:hover:not(.active){color:var(--c-text)}
|
| 34 |
-
.tab-panel{display:none}.tab-panel.active{display:block}
|
| 35 |
-
|
| 36 |
-
/* Cards */
|
| 37 |
-
.card{background:var(--c-card);border:1px solid var(--c-border);border-radius:var(--r);padding:18px 20px;margin-bottom:14px}
|
| 38 |
-
.card-header{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--c-muted);margin-bottom:14px;display:flex;align-items:center;gap:8px;padding-bottom:10px;border-bottom:1px solid var(--c-border)}
|
| 39 |
-
|
| 40 |
-
/* Grids */
|
| 41 |
-
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
| 42 |
-
.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
|
| 43 |
-
.grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
|
| 44 |
-
@media(max-width:900px){.grid-4,.grid-3{grid-template-columns:1fr 1fr}}
|
| 45 |
-
@media(max-width:600px){.grid-2,.grid-3,.grid-4{grid-template-columns:1fr}}
|
| 46 |
-
|
| 47 |
-
/* Buttons */
|
| 48 |
-
button,.btn{display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:8px 16px;border:1px solid transparent;border-radius:var(--r);font-size:13px;font-weight:600;cursor:pointer;transition:filter .12s,transform .08s;white-space:nowrap}
|
| 49 |
-
button:active{transform:scale(.97)}
|
| 50 |
-
button:disabled{opacity:.4;cursor:not-allowed}
|
| 51 |
-
.btn-primary{background:#2d3fcc;color:#e8ecff;border-color:#3d50df}
|
| 52 |
-
.btn-success{background:#1a6b3a;color:#d1fae5;border-color:#226b45}
|
| 53 |
-
.btn-warn{background:#7a4810;color:#fde7b0;border-color:#8f5515}
|
| 54 |
-
.btn-danger{background:#7a1f1f;color:#fecaca;border-color:#902828}
|
| 55 |
-
.btn-ghost{background:transparent;color:var(--c-text);border-color:var(--c-border)}
|
| 56 |
-
.btn-sm{padding:4px 10px;font-size:12px}
|
| 57 |
-
.btn-full{width:100%}
|
| 58 |
-
button:hover:not(:disabled){filter:brightness(1.12)}
|
| 59 |
-
|
| 60 |
-
/* Forms */
|
| 61 |
-
.form-group{margin-bottom:13px}
|
| 62 |
-
label{display:block;font-size:11px;font-weight:600;color:var(--c-muted);margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px}
|
| 63 |
-
input,select{width:100%;background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r);color:var(--c-text);padding:8px 11px;font-size:13px;transition:border-color .15s}
|
| 64 |
-
input:focus,select:focus{outline:none;border-color:var(--c-primary)}
|
| 65 |
-
input::placeholder{color:var(--c-muted)}
|
| 66 |
-
|
| 67 |
-
/* Response boxes */
|
| 68 |
-
.resp{margin-top:10px;padding:12px 14px;border-radius:var(--r);font-size:12px;font-family:'Consolas',monospace;background:var(--c-surface);border:1px solid var(--c-border);white-space:pre-wrap;word-break:break-all;display:none;max-height:280px;overflow-y:auto;line-height:1.6}
|
| 69 |
-
.resp.show{display:block}
|
| 70 |
-
|
| 71 |
-
/* Risk gauge */
|
| 72 |
-
.gauge-wrap{text-align:center;padding:8px 0}
|
| 73 |
-
.gauge-arc{position:relative;width:160px;height:92px;margin:0 auto}
|
| 74 |
-
.gauge-arc svg{overflow:visible}
|
| 75 |
-
.gauge-val{position:absolute;bottom:-4px;left:50%;transform:translateX(-50%);font-size:30px;font-weight:800;letter-spacing:-.5px}
|
| 76 |
-
.gauge-label{margin-top:4px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--c-muted)}
|
| 77 |
-
|
| 78 |
-
/* Security level bar */
|
| 79 |
-
.level-bar{display:flex;gap:3px;margin-top:8px}
|
| 80 |
-
.level-seg{flex:1;height:6px;border-radius:3px;background:var(--c-border);transition:background .4s}
|
| 81 |
-
.level-seg.active-0{background:#22c55e}.level-seg.active-1{background:#84cc16}.level-seg.active-2{background:#e8a020}.level-seg.active-3{background:#f97316}.level-seg.active-4{background:#e04545}
|
| 82 |
-
|
| 83 |
-
/* Step progress */
|
| 84 |
-
.steps{display:flex;gap:6px;margin-bottom:18px;overflow-x:auto}
|
| 85 |
-
.step-item{flex:1;min-width:130px;background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r);padding:10px 12px;cursor:pointer;transition:border-color .15s;text-align:center}
|
| 86 |
-
.step-item.done{border-color:var(--c-success)}.step-item.active{border-color:var(--c-primary)}
|
| 87 |
-
.step-num{width:26px;height:26px;border-radius:50%;background:var(--c-border);color:var(--c-muted);display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;margin-bottom:5px}
|
| 88 |
-
.step-item.done .step-num{background:var(--c-success);color:#fff}.step-item.active .step-num{background:var(--c-primary);color:#fff}
|
| 89 |
-
|
| 90 |
-
/* Decision panel */
|
| 91 |
-
.decision-panel{background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r);overflow:hidden}
|
| 92 |
-
.decision-header{padding:12px 16px;font-size:16px;font-weight:800;text-align:center;letter-spacing:.5px}
|
| 93 |
-
.decision-header.success{background:#0d2e1a;color:var(--c-success);border-bottom:1px solid #1a4d2c}
|
| 94 |
-
.decision-header.challenge{background:#2d1e07;color:var(--c-warn);border-bottom:1px solid #5a3b0e}
|
| 95 |
-
.decision-header.blocked{background:#2a0e0e;color:var(--c-danger);border-bottom:1px solid #5a1e1e}
|
| 96 |
-
.decision-body{padding:14px}
|
| 97 |
-
|
| 98 |
-
/* Risk factor bars */
|
| 99 |
-
.factor-row{margin-bottom:7px}
|
| 100 |
-
.factor-label{font-size:12px;color:var(--c-muted);margin-bottom:3px}
|
| 101 |
-
.factor-bar-wrap{height:8px;background:var(--c-border);border-radius:3px;overflow:hidden}
|
| 102 |
-
.factor-bar{height:100%;border-radius:3px;transition:width .7s ease}
|
| 103 |
-
|
| 104 |
-
/* Anomaly feed */
|
| 105 |
-
.anomaly-item{background:#1a0d0d;border:1px solid #3d1818;border-radius:var(--r);padding:10px 13px;margin-bottom:6px;display:flex;align-items:center;gap:10px;animation:slideIn .25s ease}
|
| 106 |
-
@keyframes slideIn{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}
|
| 107 |
-
.anomaly-type{font-weight:700;font-size:13px;color:var(--c-danger)}.anomaly-meta{font-size:11px;color:var(--c-muted);margin-top:2px}
|
| 108 |
-
|
| 109 |
-
/* Token bar */
|
| 110 |
-
.token-bar{background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r);padding:10px 14px;margin-bottom:18px;display:flex;align-items:center;gap:10px;font-size:12px}
|
| 111 |
-
.token-val{flex:1;font-family:'Consolas',monospace;color:var(--c-info);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}
|
| 112 |
-
|
| 113 |
-
/* Stat boxes */
|
| 114 |
-
.stat-box{background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r);padding:16px;text-align:center}
|
| 115 |
-
.stat-num{font-size:30px;font-weight:800;line-height:1;letter-spacing:-.5px}
|
| 116 |
-
.stat-label{font-size:10px;color:var(--c-muted);margin-top:5px;text-transform:uppercase;letter-spacing:.6px}
|
| 117 |
-
|
| 118 |
-
/* Attack log */
|
| 119 |
-
.attack-log{background:#0a0b10;border:1px solid #3d1818;border-radius:var(--r);padding:10px 12px;font-family:'Consolas',monospace;font-size:12px;color:#e04545;height:200px;overflow-y:auto;margin-top:10px}
|
| 120 |
-
.attack-log .line{margin:2px 0}.detected{color:#d4a017;font-weight:700}.att-blocked{color:#fca5a5;font-weight:700}
|
| 121 |
-
|
| 122 |
-
/* Monitoring */
|
| 123 |
-
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
| 124 |
-
.monitor-pulse{animation:pulse 1.4s ease-in-out infinite}
|
| 125 |
-
.monitor-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:3px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;transition:all .25s}
|
| 126 |
-
.monitor-badge.mon-on{background:#2a0e0e;color:#f87171;border:1px solid #5a1e1e}
|
| 127 |
-
.monitor-badge.mon-off{background:var(--c-border);color:var(--c-muted);border:1px solid transparent}
|
| 128 |
-
.monitor-log{background:#0a0b10;border:1px solid var(--c-border);border-radius:var(--r);padding:10px 12px;font-family:'Consolas',monospace;font-size:11.5px;height:145px;overflow-y:auto;margin-top:10px;line-height:1.7}
|
| 129 |
-
.monitor-log .ml-ok{color:#22c55e}.monitor-log .ml-threat{color:#e04545;font-weight:700}.monitor-log .ml-warn{color:#e8a020}.monitor-log .ml-info{color:#3ba8cc}
|
| 130 |
-
.monitor-stats{display:flex;flex-wrap:wrap;gap:8px;padding:10px 14px;background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r);font-size:12px;margin-bottom:10px}
|
| 131 |
-
.ms-item{display:flex;flex-direction:column;align-items:center;min-width:58px}
|
| 132 |
-
.ms-val{font-size:19px;font-weight:800;line-height:1;letter-spacing:-.3px}
|
| 133 |
-
.ms-lbl{font-size:10px;color:var(--c-muted);text-transform:uppercase;letter-spacing:.4px;margin-top:2px}
|
| 134 |
-
|
| 135 |
-
/* Compare layout */
|
| 136 |
-
.compare{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px}
|
| 137 |
-
.compare-side{border:1px solid var(--c-border);border-radius:var(--r);padding:13px}
|
| 138 |
-
.compare-title{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;margin-bottom:8px}
|
| 139 |
-
|
| 140 |
-
/* Callouts */
|
| 141 |
-
.callout{padding:10px 13px;border-radius:var(--r);font-size:13px;line-height:1.6;margin-bottom:13px;border-left:3px solid}
|
| 142 |
-
.callout-info{background:#08192b;border-color:var(--c-info);color:#a5d8f3}
|
| 143 |
-
.callout-warn{background:#1e1400;border-color:var(--c-warn);color:#f5d48a}
|
| 144 |
-
.callout-success{background:#0a1e12;border-color:var(--c-success);color:#a3e6bf}
|
| 145 |
-
.callout-danger{background:#1e0a0a;border-color:var(--c-danger);color:#f5a5a5}
|
| 146 |
-
|
| 147 |
-
/* Hero — no gradient text */
|
| 148 |
-
.hero{padding:28px 0 20px;border-bottom:1px solid var(--c-border);margin-bottom:24px}
|
| 149 |
-
.hero h1{font-size:22px;font-weight:700;color:var(--c-text);margin-bottom:4px;letter-spacing:-.3px}
|
| 150 |
-
.hero h1 span{color:var(--c-primary)}
|
| 151 |
-
.hero p{color:var(--c-muted);font-size:13px;margin-top:4px}
|
| 152 |
-
|
| 153 |
-
/* Scrollbars */
|
| 154 |
-
::-webkit-scrollbar{width:5px;height:5px}::-webkit-scrollbar-track{background:var(--c-surface)}::-webkit-scrollbar-thumb{background:var(--c-border);border-radius:3px}
|
| 155 |
-
|
| 156 |
-
/* Code */
|
| 157 |
-
code{background:#0d1117;border:1px solid var(--c-border);border-radius:3px;padding:1px 6px;font-size:11.5px;color:#60a5fa;font-family:'Consolas',monospace}
|
| 158 |
-
|
| 159 |
-
/* Email status pills */
|
| 160 |
-
.pill{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:3px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.4px}
|
| 161 |
-
.pill-ok{background:#0a1e12;color:var(--c-success);border:1px solid #226b45}
|
| 162 |
-
.pill-err{background:#1e0a0a;color:var(--c-danger);border:1px solid #902828}
|
| 163 |
-
.pill-warn{background:#1e1400;color:var(--c-warn);border:1px solid #8f5515}
|
| 164 |
-
</style>
|
| 165 |
-
</head>
|
| 166 |
-
<body>
|
| 167 |
-
<div class="topbar">
|
| 168 |
-
<div class="topbar-logo">⚡ AdaptiveAuth</div>
|
| 169 |
-
<div class="topbar-status">
|
| 170 |
-
<span class="dot" id="statusDot"></span>
|
| 171 |
-
<span id="statusText">Checking…</span>
|
| 172 |
-
<button class="btn btn-ghost btn-sm" onclick="ping()" style="margin-left:8px;">Refresh</button>
|
| 173 |
-
</div>
|
| 174 |
-
</div>
|
| 175 |
-
|
| 176 |
-
<div class="container">
|
| 177 |
-
<div class="hero">
|
| 178 |
-
<h1>Adaptive Authentication Framework</h1>
|
| 179 |
-
<p>Production-ready risk-based auth — JWT • 2FA • Behavioral Analysis • Anomaly Detection</p>
|
| 180 |
-
</div>
|
| 181 |
-
|
| 182 |
-
<!-- Token bar -->
|
| 183 |
-
<div class="token-bar">
|
| 184 |
-
<span style="color:var(--c-muted);font-weight:700;font-size:12px;white-space:nowrap;">JWT TOKEN</span>
|
| 185 |
-
<span class="token-val" id="tokenPreview">No token — log in first</span>
|
| 186 |
-
<button class="btn btn-ghost btn-sm" onclick="copyToken()">Copy</button>
|
| 187 |
-
<button class="btn btn-danger btn-sm" onclick="clearToken()">Clear</button>
|
| 188 |
-
</div>
|
| 189 |
-
|
| 190 |
-
<!-- Main tabs -->
|
| 191 |
-
<div class="main-tabs">
|
| 192 |
-
<button class="active" onclick="switchTab('demo1',this)">🔬 Scenario 1: Behavior</button>
|
| 193 |
-
<button onclick="switchTab('demo2',this)">🚨 Scenario 2: Attacks</button>
|
| 194 |
-
<button onclick="switchTab('api',this)">🔧 API Testing</button>
|
| 195 |
-
<button onclick="switchTab('admin',this)">🛡️ Admin Dashboard</button>
|
| 196 |
-
<button onclick="switchTab('intel',this)">🧠 Intelligence</button>
|
| 197 |
-
</div>
|
| 198 |
-
|
| 199 |
-
<!-- ====== SCENARIO 1 ====== -->
|
| 200 |
-
<div id="tab-demo1" class="tab-panel active">
|
| 201 |
-
<div class="callout callout-info">
|
| 202 |
-
<strong>Scenario 1 — User Behaviour Anomaly Detection</strong><br>
|
| 203 |
-
The demo user has <strong>30 days of normal login history</strong> from New York on Windows Chrome (Mon–Fri, 8AM–5PM).
|
| 204 |
-
We show how the framework reacts when the <em>same password</em> is used from a completely different context.
|
| 205 |
-
</div>
|
| 206 |
-
|
| 207 |
-
<div class="steps">
|
| 208 |
-
<div class="step-item" id="s1step0"><div class="step-num">0</div><div style="font-size:12px;font-weight:700;">Setup</div><div style="font-size:11px;color:var(--c-muted);">Create demo user</div></div>
|
| 209 |
-
<div class="step-item" id="s1step1"><div class="step-num">1</div><div style="font-size:12px;font-weight:700;">Normal Login</div><div style="font-size:11px;color:var(--c-muted);">Known context</div></div>
|
| 210 |
-
<div class="step-item" id="s1step2"><div class="step-num">2</div><div style="font-size:12px;font-weight:700;">Suspicious Login</div><div style="font-size:11px;color:var(--c-muted);">Unknown context</div></div>
|
| 211 |
-
<div class="step-item" id="s1step3"><div class="step-num">3</div><div style="font-size:12px;font-weight:700;">Verify Challenge</div><div style="font-size:11px;color:var(--c-muted);">Step-up auth</div></div>
|
| 212 |
-
</div>
|
| 213 |
-
|
| 214 |
-
<div class="card">
|
| 215 |
-
<div class="card-header"><span>⚙️</span> Step 0 – Setup Demo Environment</div>
|
| 216 |
-
<p style="font-size:13px;color:var(--c-muted);margin-bottom:14px;">Creates the demo user with a realistic 30-day behavioral profile (15 logins from a trusted IP, device, and time window).</p>
|
| 217 |
-
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
| 218 |
-
<button class="btn btn-primary" onclick="setupDemo(false)">🔧 Setup Demo</button>
|
| 219 |
-
<button class="btn btn-warn" onclick="setupDemo(true)">🔄 Reset & Re-setup</button>
|
| 220 |
-
<button class="btn btn-ghost" onclick="checkDemoState()">📊 Check State</button>
|
| 221 |
-
</div>
|
| 222 |
-
<div id="setupResp" class="resp"></div>
|
| 223 |
-
</div>
|
| 224 |
-
|
| 225 |
-
<div class="compare">
|
| 226 |
-
<div class="compare-side">
|
| 227 |
-
<div class="compare-title" style="color:var(--c-success);">✅ Normal Context</div>
|
| 228 |
-
<div style="font-size:13px;">
|
| 229 |
-
<div style="margin:3px 0;"><span class="tag">IP</span> 203.0.113.10 (known)</div>
|
| 230 |
-
<div style="margin:3px 0;"><span class="tag">Location</span> New York, US</div>
|
| 231 |
-
<div style="margin:3px 0;"><span class="tag">Device</span> Windows Chrome</div>
|
| 232 |
-
<div style="margin:3px 0;"><span class="tag">Time</span> Business hours</div>
|
| 233 |
-
<div style="margin:3px 0;"><span class="tag">History</span> 15 logins seen</div>
|
| 234 |
-
</div>
|
| 235 |
-
<button class="btn btn-success btn-full mt-4" onclick="doNormalLogin()">▶ Run Normal Login</button>
|
| 236 |
-
<div id="normalLoginResp" class="resp"></div>
|
| 237 |
-
</div>
|
| 238 |
-
<div class="compare-side">
|
| 239 |
-
<div class="compare-title" style="color:var(--c-danger);">🚩 Suspicious Context</div>
|
| 240 |
-
<div style="font-size:13px;">
|
| 241 |
-
<div style="margin:3px 0;"><span class="tag">IP</span> 198.51.100.55 (new!)</div>
|
| 242 |
-
<div style="margin:3px 0;"><span class="tag">Location</span> Moscow, Russia</div>
|
| 243 |
-
<div style="margin:3px 0;"><span class="tag">Device</span> iPhone Safari (new!)</div>
|
| 244 |
-
<div style="margin:3px 0;"><span class="tag">Time</span> Same password</div>
|
| 245 |
-
<div style="margin:3px 0;"><span class="tag">History</span> 0 logins from here</div>
|
| 246 |
-
</div>
|
| 247 |
-
<button class="btn btn-danger btn-full mt-4" onclick="doSuspiciousLogin()">▶ Run Suspicious Login</button>
|
| 248 |
-
<div id="suspLoginResp" class="resp"></div>
|
| 249 |
-
</div>
|
| 250 |
-
</div>
|
| 251 |
-
|
| 252 |
-
<div class="card" id="riskVizCard" style="display:none;">
|
| 253 |
-
<div class="card-header"><span>📊</span> Risk Assessment Result</div>
|
| 254 |
-
<div class="grid-2">
|
| 255 |
-
<div>
|
| 256 |
-
<div class="gauge-wrap">
|
| 257 |
-
<div class="gauge-arc">
|
| 258 |
-
<svg width="160" height="90" viewBox="0 0 160 90">
|
| 259 |
-
<path d="M 10,80 A 70,70 0 0,1 150,80" fill="none" stroke="#2e3347" stroke-width="12" stroke-linecap="round"/>
|
| 260 |
-
<path id="gaugeArc" d="M 10,80 A 70,70 0 0,1 150,80" fill="none" stroke="#6366f1" stroke-width="12" stroke-linecap="round" stroke-dasharray="220" stroke-dashoffset="220" style="transition:stroke-dashoffset .8s ease,stroke .5s;"/>
|
| 261 |
-
</svg>
|
| 262 |
-
<div class="gauge-val" id="gaugeNum" style="color:var(--c-primary);">0</div>
|
| 263 |
-
</div>
|
| 264 |
-
<div class="gauge-label" id="gaugeLabel">Risk Score</div>
|
| 265 |
-
</div>
|
| 266 |
-
</div>
|
| 267 |
-
<div>
|
| 268 |
-
<div style="font-size:12px;color:var(--c-muted);margin-bottom:4px;">Security Level</div>
|
| 269 |
-
<div class="level-bar" id="levelBar">
|
| 270 |
-
<div class="level-seg" id="lseg0"></div><div class="level-seg" id="lseg1"></div><div class="level-seg" id="lseg2"></div><div class="level-seg" id="lseg3"></div><div class="level-seg" id="lseg4"></div>
|
| 271 |
-
</div>
|
| 272 |
-
<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--c-muted);margin-top:3px;"><span>0 Trusted</span><span>4 Blocked</span></div>
|
| 273 |
-
<div style="margin-top:12px;" id="decisionBox"></div>
|
| 274 |
-
</div>
|
| 275 |
-
</div>
|
| 276 |
-
<div style="margin-top:16px;">
|
| 277 |
-
<div style="font-size:12px;color:var(--c-muted);font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;">Risk Factor Breakdown</div>
|
| 278 |
-
<div id="factorBars"></div>
|
| 279 |
-
</div>
|
| 280 |
-
<div id="triggeredRules" style="margin-top:12px;"></div>
|
| 281 |
-
</div>
|
| 282 |
-
|
| 283 |
-
<div class="card" id="challengeCard" style="display:none;">
|
| 284 |
-
<div class="card-header"><span>🔐</span> Step 3 – Complete Step-up Challenge</div>
|
| 285 |
-
<div class="callout callout-warn">
|
| 286 |
-
The framework triggered a challenge because of the suspicious context.
|
| 287 |
-
In a live deployment this sends a real email. In the demo, use code <strong>000000</strong>.
|
| 288 |
-
</div>
|
| 289 |
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
| 290 |
-
<div>
|
| 291 |
-
<div class="form-group"><label>Challenge ID</label><input id="challengeId" type="text" placeholder="Auto-filled from step 2" readonly></div>
|
| 292 |
-
<div class="form-group"><label>Verification Code</label><input id="challengeCode" type="text" placeholder="Enter code (000000 for demo)"></div>
|
| 293 |
-
<button class="btn btn-primary btn-full" onclick="doCompleteChallenge()">✅ Verify & Complete Login</button>
|
| 294 |
-
</div>
|
| 295 |
-
<div style="font-size:13px;color:var(--c-muted);">
|
| 296 |
-
<p><strong>Why was this required?</strong></p>
|
| 297 |
-
<ul style="margin-top:8px;padding-left:18px;line-height:2;">
|
| 298 |
-
<li>Unknown IP address</li><li>New device fingerprint</li><li>Geographic location changed</li><li>Security Level ≥ 2 → challenge required</li>
|
| 299 |
-
</ul>
|
| 300 |
-
</div>
|
| 301 |
-
</div>
|
| 302 |
-
<div id="challengeResp" class="resp"></div>
|
| 303 |
-
</div>
|
| 304 |
-
</div><!-- /demo1 -->
|
| 305 |
-
|
| 306 |
-
<!-- ====== SCENARIO 2 ====== -->
|
| 307 |
-
<div id="tab-demo2" class="tab-panel">
|
| 308 |
-
<div class="callout callout-danger">
|
| 309 |
-
<strong>Scenario 2 — Attack & Anomaly Detection</strong><br>
|
| 310 |
-
We simulate a brute-force attack from an attacker IP, watch the <code>AnomalyDetector</code> fire in real time,
|
| 311 |
-
then show how a <em>legitimate user</em> is treated vs the <em>attacker with the correct password</em>.
|
| 312 |
-
</div>
|
| 313 |
-
|
| 314 |
-
<div class="steps">
|
| 315 |
-
<div class="step-item" id="s2step1"><div class="step-num">1</div><div style="font-size:12px;font-weight:700;">Brute Force</div><div style="font-size:11px;color:var(--c-muted);">Inject failed logins</div></div>
|
| 316 |
-
<div class="step-item" id="s2step2"><div class="step-num">2</div><div style="font-size:12px;font-weight:700;">Legit User</div><div style="font-size:11px;color:var(--c-muted);">Normal login during attack</div></div>
|
| 317 |
-
<div class="step-item" id="s2step3"><div class="step-num">3</div><div style="font-size:12px;font-weight:700;">Attacker Unmasked</div><div style="font-size:11px;color:var(--c-muted);">Correct password, still blocked</div></div>
|
| 318 |
-
</div>
|
| 319 |
-
|
| 320 |
-
<div class="grid-2">
|
| 321 |
-
<div>
|
| 322 |
-
<div class="card">
|
| 323 |
-
<div class="card-header"><span>💣</span> Step 1 – Simulate Brute Force Attack</div>
|
| 324 |
-
<p style="font-size:13px;color:var(--c-muted);margin-bottom:12px;">Injects failed login attempts from attacker IP <code>192.0.2.100</code> (Beijing, China), then triggers the AnomalyDetector.</p>
|
| 325 |
-
<div class="form-group"><label>Number of attempts</label><input id="attackAttempts" type="number" value="12" min="5" max="25"></div>
|
| 326 |
-
<button class="btn btn-danger btn-full" onclick="doSimulateAttack()">💥 Launch Attack Simulation</button>
|
| 327 |
-
<div class="attack-log" id="attackLog" style="display:none;"></div>
|
| 328 |
-
<div id="attackResp" class="resp"></div>
|
| 329 |
-
</div>
|
| 330 |
-
<div class="card">
|
| 331 |
-
<div class="card-header" style="justify-content:space-between;">
|
| 332 |
-
<span>🚨 Live Anomaly Feed & Monitoring</span>
|
| 333 |
-
<span id="monitorBadge" class="monitor-badge mon-off">⏸ Idle</span>
|
| 334 |
-
</div>
|
| 335 |
-
<!-- Controls -->
|
| 336 |
-
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;align-items:center;">
|
| 337 |
-
<button id="monitorBtn" class="btn btn-success btn-sm" onclick="toggleMonitoring()">▶ Start Monitoring</button>
|
| 338 |
-
<button class="btn btn-ghost btn-sm" onclick="refreshAnomalies()">🔄 Refresh Feed</button>
|
| 339 |
-
<button class="btn btn-danger btn-sm" onclick="clearAnomalies()">🗑 Clear</button>
|
| 340 |
-
<span style="font-size:11px;color:var(--c-muted);">Runs a full scan cycle every 2 s when active</span>
|
| 341 |
-
</div>
|
| 342 |
-
<!-- Stats strip (shown while monitoring active) -->
|
| 343 |
-
<div class="monitor-stats" id="monitorStats" style="display:none;">
|
| 344 |
-
<div class="ms-item"><div class="ms-val" id="msThreatVal" style="color:var(--c-danger);">—</div><div class="ms-lbl">Threat</div></div>
|
| 345 |
-
<div class="ms-item"><div class="ms-val" id="msAnomalyVal" style="color:var(--c-warn);">—</div><div class="ms-lbl">Anomalies</div></div>
|
| 346 |
-
<div class="ms-item"><div class="ms-val" id="msFailedVal" style="color:var(--c-danger);">—</div><div class="ms-lbl">Failed/1h</div></div>
|
| 347 |
-
<div class="ms-item"><div class="ms-val" id="msSessionVal" style="color:var(--c-info);">—</div><div class="ms-lbl">Sessions</div></div>
|
| 348 |
-
<div class="ms-item"><div class="ms-val" id="msSuspVal" style="color:var(--c-warn);">—</div><div class="ms-lbl">Suspicious</div></div>
|
| 349 |
-
<div class="ms-item" style="margin-left:auto;"><div class="ms-val" id="msCycleVal" style="font-size:11px;color:var(--c-muted);">—</div><div class="ms-lbl">Last cycle</div></div>
|
| 350 |
-
</div>
|
| 351 |
-
<!-- Monitoring log -->
|
| 352 |
-
<div class="monitor-log" id="monitorLog" style="display:none;"></div>
|
| 353 |
-
<!-- Anomaly feed -->
|
| 354 |
-
<div id="anomalyFeed"><p style="color:var(--c-muted);font-size:13px;">No anomalies yet. Run the attack simulation above.</p></div>
|
| 355 |
-
</div>
|
| 356 |
-
</div>
|
| 357 |
-
<div>
|
| 358 |
-
<div class="card">
|
| 359 |
-
<div class="card-header"><span>👤</span> Step 2 – Legitimate User Logs In</div>
|
| 360 |
-
<p style="font-size:13px;color:var(--c-muted);margin-bottom:12px;">Same account. Logs in from <strong>New York (trusted IP, trusted device)</strong> while the attack is in progress.</p>
|
| 361 |
-
<button class="btn btn-success btn-full" onclick="doLegitLogin()">▶ Login as Legitimate User</button>
|
| 362 |
-
<div id="legitResp" class="resp"></div>
|
| 363 |
-
</div>
|
| 364 |
-
<div class="card">
|
| 365 |
-
<div class="card-header"><span>🤖</span> Step 3 – Attacker Uses Correct Password</div>
|
| 366 |
-
<p style="font-size:13px;color:var(--c-muted);margin-bottom:12px;">The attacker somehow obtained the real password. See what happens.<br><strong>Spoiler:</strong> correct password alone is not enough.</p>
|
| 367 |
-
<button class="btn btn-warn btn-full" onclick="doAttackerLogin()">🔑 Attacker Login Attempt</button>
|
| 368 |
-
<div id="attackerLoginResp" class="resp"></div>
|
| 369 |
-
</div>
|
| 370 |
-
<div class="card">
|
| 371 |
-
<div class="card-header"><span>⚖️</span> Side-by-side Comparison</div>
|
| 372 |
-
<div id="compareBox" style="font-size:13px;color:var(--c-muted);">Run steps 2 and 3 to see the comparison.</div>
|
| 373 |
-
</div>
|
| 374 |
-
</div>
|
| 375 |
-
</div>
|
| 376 |
-
</div><!-- /demo2 -->
|
| 377 |
-
|
| 378 |
-
<!-- ====== API TESTING ====== -->
|
| 379 |
-
<div id="tab-api" class="tab-panel">
|
| 380 |
-
<div class="grid-2">
|
| 381 |
-
<div class="card">
|
| 382 |
-
<div class="card-header"><span>📝</span> Register</div>
|
| 383 |
-
<div class="form-group"><label>Email</label><input id="regEmail" type="email" placeholder="user@example.com"></div>
|
| 384 |
-
<div class="form-group"><label>Password</label><input id="regPassword" type="password" placeholder="Min 8 chars, upper, lower, digit"></div>
|
| 385 |
-
<div class="form-group"><label>Full Name</label><input id="regName" type="text" placeholder="Optional"></div>
|
| 386 |
-
<button class="btn btn-primary btn-full" onclick="apiRegister()">Register</button>
|
| 387 |
-
<div id="regResp" class="resp"></div>
|
| 388 |
-
</div>
|
| 389 |
-
<div class="card">
|
| 390 |
-
<div class="card-header"><span>🔑</span> Login</div>
|
| 391 |
-
<div class="form-group"><label>Email</label><input id="loginEmail" type="email" placeholder="user@example.com"></div>
|
| 392 |
-
<div class="form-group"><label>Password</label><input id="loginPassword" type="password" placeholder="Password"></div>
|
| 393 |
-
<div style="display:flex;gap:8px;">
|
| 394 |
-
<button class="btn btn-primary" style="flex:1;" onclick="apiLogin()">Login</button>
|
| 395 |
-
<button class="btn btn-ghost" style="flex:1;" onclick="apiAdaptiveLogin()">Adaptive Login</button>
|
| 396 |
-
</div>
|
| 397 |
-
<div id="loginResp" class="resp"></div>
|
| 398 |
-
</div>
|
| 399 |
-
<div class="card">
|
| 400 |
-
<div class="card-header"><span>🔐</span> Two-Factor Auth (TOTP)</div>
|
| 401 |
-
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px;">
|
| 402 |
-
<button class="btn btn-primary btn-sm" onclick="api2faEnable()">Enable 2FA</button>
|
| 403 |
-
<button class="btn btn-ghost btn-sm" onclick="api2faStatus()">Status</button>
|
| 404 |
-
<button class="btn btn-danger btn-sm" onclick="api2faDisable()">Disable</button>
|
| 405 |
-
</div>
|
| 406 |
-
<div id="qrWrap" style="text-align:center;margin:10px 0;"></div>
|
| 407 |
-
<div class="form-group"><label>TOTP Code</label><input id="totpCode" type="text" placeholder="6-digit code from authenticator"></div>
|
| 408 |
-
<button class="btn btn-success btn-full" onclick="api2faVerify()">Verify & Activate</button>
|
| 409 |
-
<div id="tfaResp" class="resp"></div>
|
| 410 |
-
</div>
|
| 411 |
-
<div class="card">
|
| 412 |
-
<div class="card-header"><span>👤</span> My Profile</div>
|
| 413 |
-
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px;">
|
| 414 |
-
<button class="btn btn-ghost btn-sm" onclick="apiGetProfile()">Profile</button>
|
| 415 |
-
<button class="btn btn-ghost btn-sm" onclick="apiGetSecurity()">Security</button>
|
| 416 |
-
<button class="btn btn-ghost btn-sm" onclick="apiGetDevices()">Devices</button>
|
| 417 |
-
<button class="btn btn-ghost btn-sm" onclick="apiGetSessions()">Sessions</button>
|
| 418 |
-
<button class="btn btn-danger btn-sm" onclick="apiRevokeAll()">Revoke All</button>
|
| 419 |
-
</div>
|
| 420 |
-
<div id="profileResp" class="resp"></div>
|
| 421 |
-
</div>
|
| 422 |
-
<div class="card">
|
| 423 |
-
<div class="card-header"><span>🔒</span> Password Management</div>
|
| 424 |
-
<div class="form-group"><label>Current Password</label><input id="curPwd" type="password"></div>
|
| 425 |
-
<div class="form-group"><label>New Password</label><input id="newPwd" type="password"></div>
|
| 426 |
-
<button class="btn btn-warn btn-full" onclick="apiChangePwd()">Change Password</button>
|
| 427 |
-
<hr style="border-color:var(--c-border);margin:14px 0;">
|
| 428 |
-
<div class="form-group"><label>Email for Reset Link</label><input id="resetEmail" type="email"></div>
|
| 429 |
-
<button class="btn btn-ghost btn-full" onclick="apiForgotPwd()">Send Reset Email</button>
|
| 430 |
-
<div id="pwdResp" class="resp"></div>
|
| 431 |
-
</div>
|
| 432 |
-
<div class="card">
|
| 433 |
-
<div class="card-header"><span>🛡️</span> Protected Endpoints</div>
|
| 434 |
-
<p style="font-size:13px;color:var(--c-muted);margin-bottom:12px;">Test JWT-protected routes. Must have a valid token saved.</p>
|
| 435 |
-
<div style="display:flex;flex-wrap:wrap;gap:8px;">
|
| 436 |
-
<button class="btn btn-success btn-sm" onclick="apiProtected()">Test /protected</button>
|
| 437 |
-
<button class="btn btn-warn btn-sm" onclick="apiAdminOnly()">Test /admin-only</button>
|
| 438 |
-
<button class="btn btn-ghost btn-sm" onclick="apiRiskProfile()">Risk Profile</button>
|
| 439 |
-
<button class="btn btn-ghost btn-sm" onclick="apiLogout()">Logout</button>
|
| 440 |
-
</div>
|
| 441 |
-
<div id="protResp" class="resp"></div>
|
| 442 |
-
</div>
|
| 443 |
-
</div>
|
| 444 |
-
</div><!-- /api -->
|
| 445 |
-
|
| 446 |
-
<!-- ====== ADMIN ====== -->
|
| 447 |
-
<div id="tab-admin" class="tab-panel">
|
| 448 |
-
<div class="callout callout-warn">
|
| 449 |
-
Admin endpoints require an admin JWT token. Login with <code>demo.admin@adaptive.demo</code> / <code>Admin@Demo456!</code> first.
|
| 450 |
-
</div>
|
| 451 |
-
<div class="card">
|
| 452 |
-
<div class="card-header"><span>🔑</span> Quick Admin Login</div>
|
| 453 |
-
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
|
| 454 |
-
<span style="font-size:13px;">credentials: <code>demo.admin@adaptive.demo</code> / <code>Admin@Demo456!</code></span>
|
| 455 |
-
<button class="btn btn-primary btn-sm" onclick="quickAdminLogin()">Login as Admin</button>
|
| 456 |
-
</div>
|
| 457 |
-
<div id="adminLoginResp" class="resp"></div>
|
| 458 |
-
</div>
|
| 459 |
-
<button class="btn btn-ghost" style="margin-bottom:12px;" onclick="loadAdminStats()">🔄 Load Statistics</button>
|
| 460 |
-
<div class="grid-4 mb-4" id="statsGrid">
|
| 461 |
-
<div class="stat-box"><div class="stat-num" style="color:var(--c-info);">—</div><div class="stat-label">Total Users</div></div>
|
| 462 |
-
<div class="stat-box"><div class="stat-num" style="color:var(--c-success);">—</div><div class="stat-label">Active Sessions</div></div>
|
| 463 |
-
<div class="stat-box"><div class="stat-num" style="color:var(--c-danger);">—</div><div class="stat-label">High Risk Today</div></div>
|
| 464 |
-
<div class="stat-box"><div class="stat-num" style="color:var(--c-warn);">—</div><div class="stat-label">Failed Logins</div></div>
|
| 465 |
-
</div>
|
| 466 |
-
<div class="card" id="emailStatusCard">
|
| 467 |
-
<div class="card-header" style="justify-content:space-between;">
|
| 468 |
-
<span style="display:flex;align-items:center;gap:8px;">✉️ Email Service Status</span>
|
| 469 |
-
<button class="btn btn-ghost btn-sm" onclick="checkEmailStatus()">🔄 Refresh</button>
|
| 470 |
-
</div>
|
| 471 |
-
<div id="emailStatusBody" style="font-size:13px;color:var(--c-muted);">Click Refresh to check email configuration.</div>
|
| 472 |
-
</div>
|
| 473 |
-
<div class="grid-2">
|
| 474 |
-
<div class="card">
|
| 475 |
-
<div class="card-header"><span>👥</span> User Management</div>
|
| 476 |
-
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;">
|
| 477 |
-
<button class="btn btn-ghost btn-sm" onclick="adminGetUsers()">List Users</button>
|
| 478 |
-
<button class="btn btn-ghost btn-sm" onclick="adminGetSessions()">List Sessions</button>
|
| 479 |
-
</div>
|
| 480 |
-
<div class="form-group"><label>User ID</label><input id="adminUserId" type="number" placeholder="User ID"></div>
|
| 481 |
-
<div style="display:flex;gap:8px;">
|
| 482 |
-
<button class="btn btn-success btn-sm" style="flex:1;" onclick="adminUnblock()">Unblock</button>
|
| 483 |
-
<button class="btn btn-danger btn-sm" style="flex:1;" onclick="adminBlock()">Block</button>
|
| 484 |
-
</div>
|
| 485 |
-
<div id="adminUserResp" class="resp"></div>
|
| 486 |
-
</div>
|
| 487 |
-
<div class="card">
|
| 488 |
-
<div class="card-header"><span>⚠️</span> Risk Events & Anomalies</div>
|
| 489 |
-
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;">
|
| 490 |
-
<button class="btn btn-warn btn-sm" onclick="adminGetRiskEvents()">Risk Events</button>
|
| 491 |
-
<button class="btn btn-danger btn-sm" onclick="adminGetAnomalies()">Active Anomalies</button>
|
| 492 |
-
<button class="btn btn-ghost btn-sm" onclick="adminRiskOverview()">Overview</button>
|
| 493 |
-
</div>
|
| 494 |
-
<div id="adminRiskResp" class="resp"></div>
|
| 495 |
-
</div>
|
| 496 |
-
</div>
|
| 497 |
-
<div class="card">
|
| 498 |
-
<div class="card-header"><span>📈</span> Risk Statistics</div>
|
| 499 |
-
<div style="display:flex;gap:8px;margin-bottom:10px;">
|
| 500 |
-
<button class="btn btn-ghost btn-sm" onclick="adminRiskStatsPeriod('day')">Day</button>
|
| 501 |
-
<button class="btn btn-ghost btn-sm" onclick="adminRiskStatsPeriod('week')">Week</button>
|
| 502 |
-
<button class="btn btn-ghost btn-sm" onclick="adminRiskStatsPeriod('month')">Month</button>
|
| 503 |
-
</div>
|
| 504 |
-
<div id="statsResp" class="resp"></div>
|
| 505 |
-
</div>
|
| 506 |
-
</div><!-- /admin -->
|
| 507 |
-
|
| 508 |
-
<!-- ====== INTELLIGENCE ====== -->
|
| 509 |
-
<div id="tab-intel" class="tab-panel">
|
| 510 |
-
<div class="callout callout-info">
|
| 511 |
-
<strong>🧠 Session Intelligence — 8 Advanced Security Features</strong><br>
|
| 512 |
-
Continuous Verification • Behavioral Intelligence • Dynamic Trust Score •
|
| 513 |
-
Micro-Challenges • Explainability • AI Anomaly Detection • Impossible Travel • Privacy-First Design.
|
| 514 |
-
</div>
|
| 515 |
-
|
| 516 |
-
<!-- Quick login -->
|
| 517 |
-
<div class="card" style="margin-bottom:10px;">
|
| 518 |
-
<div class="card-header"><span>🔑</span> Session Authentication</div>
|
| 519 |
-
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
|
| 520 |
-
<span style="font-size:13px;color:var(--c-muted);">Protected features require a JWT token.</span>
|
| 521 |
-
<button class="btn btn-primary btn-sm" onclick="intelQuickLogin()">⚡ Quick Login (demo user)</button>
|
| 522 |
-
<div id="intelLoginStatus" style="font-size:12px;"></div>
|
| 523 |
-
</div>
|
| 524 |
-
</div>
|
| 525 |
-
|
| 526 |
-
<!-- Trust Score + Continuous Verify -->
|
| 527 |
-
<div class="card">
|
| 528 |
-
<div class="card-header"><span>🛡️</span> Dynamic Trust Score & Continuous Verification</div>
|
| 529 |
-
<div style="display:flex;gap:20px;align-items:flex-start;flex-wrap:wrap;">
|
| 530 |
-
<div style="text-align:center;min-width:155px;">
|
| 531 |
-
<svg width="155" height="95" viewBox="-5 -5 165 105">
|
| 532 |
-
<path d="M 10 80 A 65 65 0 0 1 140 80" stroke="#1e293b" stroke-width="16" fill="none"/>
|
| 533 |
-
<path id="trustArc" d="M 10 80 A 65 65 0 0 1 140 80" stroke="#22c55e" stroke-width="16" fill="none"
|
| 534 |
-
stroke-dasharray="204" stroke-dashoffset="204" stroke-linecap="round"
|
| 535 |
-
style="transition:stroke-dashoffset .6s,stroke .6s;"/>
|
| 536 |
-
<text id="trustNum" x="75" y="77" text-anchor="middle" font-size="28" font-weight="700" fill="#f1f5f9">--</text>
|
| 537 |
-
<text x="75" y="93" text-anchor="middle" font-size="10" fill="#64748b">/ 100</text>
|
| 538 |
-
</svg>
|
| 539 |
-
<div id="trustLabel" style="font-size:11px;font-weight:700;color:var(--c-muted);">LOADING…</div>
|
| 540 |
-
</div>
|
| 541 |
-
<div style="flex:1;min-width:180px;">
|
| 542 |
-
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px;">
|
| 543 |
-
<button class="btn btn-ghost btn-sm" onclick="intelGetTrust()">🔄 Refresh</button>
|
| 544 |
-
<button class="btn btn-ghost btn-sm" onclick="intelContinuousVerify()">✔ Verify Now</button>
|
| 545 |
-
<button class="btn btn-warn btn-sm" onclick="intelDropTrust()">🔽 Drop to 25</button>
|
| 546 |
-
</div>
|
| 547 |
-
<div id="trustHistoryWrap" style="max-height:130px;overflow-y:auto;font-size:11px;"></div>
|
| 548 |
-
</div>
|
| 549 |
-
</div>
|
| 550 |
-
<div id="trustResp" class="resp"></div>
|
| 551 |
-
</div>
|
| 552 |
-
|
| 553 |
-
<!-- Privacy-First Behavior Monitor -->
|
| 554 |
-
<div class="card">
|
| 555 |
-
<div class="card-header"><span>🔒</span> Privacy-First Behavioral Intelligence</div>
|
| 556 |
-
<div class="callout callout-info" style="font-size:12px;padding:8px 12px;margin-bottom:10px;">
|
| 557 |
-
<strong>🔒 Privacy-First:</strong> Keystroke timings, mouse coords and scroll deltas are processed
|
| 558 |
-
<em>entirely in-browser</em>. Only the aggregated 0–1 scores are sent to the server.
|
| 559 |
-
</div>
|
| 560 |
-
<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;">
|
| 561 |
-
<button class="btn btn-success btn-sm" id="behaviorCollectBtn" onclick="toggleBehaviorCollector()">▶ Start Collecting</button>
|
| 562 |
-
<button class="btn btn-primary btn-sm" onclick="intelSendBehavior()">📤 Send Signals</button>
|
| 563 |
-
<span id="collectStatus" style="font-size:12px;color:var(--c-muted);align-self:center;"></span>
|
| 564 |
-
</div>
|
| 565 |
-
<div style="display:grid;gap:10px;">
|
| 566 |
-
<div>
|
| 567 |
-
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:3px;">
|
| 568 |
-
<span>⌨️ Typing Entropy <span style="color:var(--c-muted);">(1.0 = human-like rhythm)</span></span>
|
| 569 |
-
<span id="teVal">--</span>
|
| 570 |
-
</div>
|
| 571 |
-
<div style="height:8px;background:#1e293b;border-radius:4px;overflow:hidden;">
|
| 572 |
-
<div id="teBar" style="height:100%;width:0%;background:#22c55e;transition:width .5s,background .5s;border-radius:4px;"></div>
|
| 573 |
-
</div>
|
| 574 |
-
</div>
|
| 575 |
-
<div>
|
| 576 |
-
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:3px;">
|
| 577 |
-
<span>🖱️ Mouse Linearity <span style="color:var(--c-muted);">(1.0 = curved/natural)</span></span>
|
| 578 |
-
<span id="mlVal">--</span>
|
| 579 |
-
</div>
|
| 580 |
-
<div style="height:8px;background:#1e293b;border-radius:4px;overflow:hidden;">
|
| 581 |
-
<div id="mlBar" style="height:100%;width:0%;background:#22c55e;transition:width .5s,background .5s;border-radius:4px;"></div>
|
| 582 |
-
</div>
|
| 583 |
-
</div>
|
| 584 |
-
<div>
|
| 585 |
-
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:3px;">
|
| 586 |
-
<span>📜 Scroll Variance <span style="color:var(--c-muted);">(0.5 = organic human rhythm)</span></span>
|
| 587 |
-
<span id="svVal">--</span>
|
| 588 |
-
</div>
|
| 589 |
-
<div style="height:8px;background:#1e293b;border-radius:4px;overflow:hidden;">
|
| 590 |
-
<div id="svBar" style="height:100%;width:0%;background:#22c55e;transition:width .5s,background .5s;border-radius:4px;"></div>
|
| 591 |
-
</div>
|
| 592 |
-
</div>
|
| 593 |
-
</div>
|
| 594 |
-
<div id="behaviorResp" class="resp" style="margin-top:10px;"></div>
|
| 595 |
-
</div>
|
| 596 |
-
|
| 597 |
-
<!-- Impossible Travel + AI Anomaly (2-col) -->
|
| 598 |
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px;">
|
| 599 |
-
<!-- Impossible Travel -->
|
| 600 |
-
<div class="card" style="margin:0;">
|
| 601 |
-
<div class="card-header"><span>✈️</span> Impossible Travel Detector</div>
|
| 602 |
-
<div style="display:grid;gap:8px;">
|
| 603 |
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;">
|
| 604 |
-
<div>
|
| 605 |
-
<label style="font-size:11px;color:var(--c-muted);">FROM city</label>
|
| 606 |
-
<select id="travelFrom" class="form-input" style="margin-top:3px;width:100%;"></select>
|
| 607 |
-
</div>
|
| 608 |
-
<div>
|
| 609 |
-
<label style="font-size:11px;color:var(--c-muted);">TO city</label>
|
| 610 |
-
<select id="travelTo" class="form-input" style="margin-top:3px;width:100%;"></select>
|
| 611 |
-
</div>
|
| 612 |
-
</div>
|
| 613 |
-
<div>
|
| 614 |
-
<label style="font-size:11px;color:var(--c-muted);">Time gap (hours)</label>
|
| 615 |
-
<input id="travelHours" class="form-input" type="number" value="2" min="0.01" step="0.5" style="width:100%;margin-top:3px;">
|
| 616 |
-
</div>
|
| 617 |
-
<button class="btn btn-primary btn-sm" onclick="intelCheckTravel()">📏 Calculate Travel Risk</button>
|
| 618 |
-
</div>
|
| 619 |
-
<div id="travelResult" style="margin-top:10px;font-size:13px;"></div>
|
| 620 |
-
</div>
|
| 621 |
-
|
| 622 |
-
<!-- AI Anomaly Scorer -->
|
| 623 |
-
<div class="card" style="margin:0;">
|
| 624 |
-
<div class="card-header"><span>🤖</span> AI Anomaly Scorer</div>
|
| 625 |
-
<div style="display:grid;gap:6px;font-size:12px;">
|
| 626 |
-
<div style="display:grid;grid-template-columns:1fr auto;gap:6px;align-items:center;">
|
| 627 |
-
<span>Typing entropy</span>
|
| 628 |
-
<input id="aiTyping" type="number" class="form-input" value="0.70" min="0" max="1" step="0.05" style="width:72px;text-align:right;padding:3px 6px;">
|
| 629 |
-
</div>
|
| 630 |
-
<div style="display:grid;grid-template-columns:1fr auto;gap:6px;align-items:center;">
|
| 631 |
-
<span>Mouse linearity</span>
|
| 632 |
-
<input id="aiMouse" type="number" class="form-input" value="0.62" min="0" max="1" step="0.05" style="width:72px;text-align:right;padding:3px 6px;">
|
| 633 |
-
</div>
|
| 634 |
-
<div style="display:grid;grid-template-columns:1fr auto;gap:6px;align-items:center;">
|
| 635 |
-
<span>Scroll variance</span>
|
| 636 |
-
<input id="aiScroll" type="number" class="form-input" value="0.48" min="0" max="1" step="0.05" style="width:72px;text-align:right;padding:3px 6px;">
|
| 637 |
-
</div>
|
| 638 |
-
<div style="display:grid;grid-template-columns:1fr auto;gap:6px;align-items:center;">
|
| 639 |
-
<span>Hour norm. <span style="color:var(--c-muted);">(0=midnight, 1=noon)</span></span>
|
| 640 |
-
<input id="aiHour" type="number" class="form-input" value="0.55" min="0" max="1" step="0.05" style="width:72px;text-align:right;padding:3px 6px;">
|
| 641 |
-
</div>
|
| 642 |
-
<div style="display:grid;grid-template-columns:1fr auto;gap:6px;align-items:center;">
|
| 643 |
-
<span>Failed attempts <span style="color:var(--c-muted);">(÷20)</span></span>
|
| 644 |
-
<input id="aiFailed" type="number" class="form-input" value="0.00" min="0" max="1" step="0.05" style="width:72px;text-align:right;padding:3px 6px;">
|
| 645 |
-
</div>
|
| 646 |
-
<button class="btn btn-primary btn-sm" onclick="intelScoreAnomaly()">🧠 Score with AI</button>
|
| 647 |
-
</div>
|
| 648 |
-
<div id="anomalyResult" style="margin-top:10px;"></div>
|
| 649 |
-
</div>
|
| 650 |
-
</div>
|
| 651 |
-
|
| 652 |
-
<!-- Micro-Challenges -->
|
| 653 |
-
<div class="card">
|
| 654 |
-
<div class="card-header"><span>🧩</span> Low-Friction Micro-Challenges</div>
|
| 655 |
-
<p style="font-size:13px;color:var(--c-muted);margin-bottom:10px;">
|
| 656 |
-
Challenges fire <em>only when trust drops below 40</em> — never interrupts a trusted session.
|
| 657 |
-
</p>
|
| 658 |
-
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;">
|
| 659 |
-
<button class="btn btn-warn btn-sm" onclick="intelDropTrust()">🔽 Drop Trust to 25</button>
|
| 660 |
-
<button class="btn btn-primary btn-sm" onclick="intelGenerateChallenge()">🧩 Generate Challenge</button>
|
| 661 |
-
</div>
|
| 662 |
-
<div id="challengePanel" style="display:none;" class="callout callout-info">
|
| 663 |
-
<div style="font-size:16px;font-weight:700;margin-bottom:10px;" id="challengeQuestion">…</div>
|
| 664 |
-
<div style="display:flex;gap:8px;align-items:center;">
|
| 665 |
-
<input id="challengeAnswer" class="form-input" type="text" placeholder="Your answer…" style="width:160px;">
|
| 666 |
-
<button class="btn btn-success btn-sm" onclick="intelVerifyChallenge()">✔ Verify</button>
|
| 667 |
-
</div>
|
| 668 |
-
<div id="challengeResultMsg" style="font-size:12px;margin-top:8px;"></div>
|
| 669 |
-
</div>
|
| 670 |
-
<div id="challengeResp" class="resp"></div>
|
| 671 |
-
</div>
|
| 672 |
-
|
| 673 |
-
<!-- Explainability (demo — no auth) -->
|
| 674 |
-
<div class="card">
|
| 675 |
-
<div class="card-header"><span>📊</span> Explainable Risk Transparency</div>
|
| 676 |
-
<p style="font-size:13px;color:var(--c-muted);margin-bottom:10px;">
|
| 677 |
-
Submit factor scores and see exactly which signals contributed and why — with model weights.
|
| 678 |
-
</p>
|
| 679 |
-
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:10px;font-size:12px;">
|
| 680 |
-
<div>
|
| 681 |
-
<label style="color:var(--c-muted);">🌍 Location (0–100)</label>
|
| 682 |
-
<input id="expLocation" type="number" class="form-input" value="85" min="0" max="100" style="width:100%;margin-top:4px;">
|
| 683 |
-
</div>
|
| 684 |
-
<div>
|
| 685 |
-
<label style="color:var(--c-muted);">💻 Device</label>
|
| 686 |
-
<input id="expDevice" type="number" class="form-input" value="15" min="0" max="100" style="width:100%;margin-top:4px;">
|
| 687 |
-
</div>
|
| 688 |
-
<div>
|
| 689 |
-
<label style="color:var(--c-muted);">🕐 Time</label>
|
| 690 |
-
<input id="expTime" type="number" class="form-input" value="10" min="0" max="100" style="width:100%;margin-top:4px;">
|
| 691 |
-
</div>
|
| 692 |
-
<div>
|
| 693 |
-
<label style="color:var(--c-muted);">⚡ Velocity</label>
|
| 694 |
-
<input id="expVelocity" type="number" class="form-input" value="5" min="0" max="100" style="width:100%;margin-top:4px;">
|
| 695 |
-
</div>
|
| 696 |
-
<div>
|
| 697 |
-
<label style="color:var(--c-muted);">🧠 Behavior</label>
|
| 698 |
-
<input id="expBehavior" type="number" class="form-input" value="20" min="0" max="100" style="width:100%;margin-top:4px;">
|
| 699 |
-
</div>
|
| 700 |
-
<div>
|
| 701 |
-
<label style="color:var(--c-muted);">Security level (0–4)</label>
|
| 702 |
-
<input id="expLevel" type="number" class="form-input" value="2" min="0" max="4" style="width:100%;margin-top:4px;">
|
| 703 |
-
</div>
|
| 704 |
-
</div>
|
| 705 |
-
<button class="btn btn-primary btn-sm" onclick="intelExplain()">🔍 Generate Explanation</button>
|
| 706 |
-
<div id="explainResult" style="margin-top:12px;"></div>
|
| 707 |
-
</div>
|
| 708 |
-
|
| 709 |
-
<!-- Session Audit Trail (authed) -->
|
| 710 |
-
<div class="card">
|
| 711 |
-
<div class="card-header"><span>📋</span> Session Audit Trail <span style="color:var(--c-muted);font-size:12px;">(requires login)</span></div>
|
| 712 |
-
<button class="btn btn-ghost btn-sm" onclick="intelSessionExplain()" style="margin-bottom:8px;">📄 Fetch My Session Events</button>
|
| 713 |
-
<div id="sessionExplainResp" class="resp"></div>
|
| 714 |
-
</div>
|
| 715 |
-
</div><!-- /intel -->
|
| 716 |
-
</div><!-- /container -->
|
| 717 |
-
|
| 718 |
-
<script>
|
| 719 |
-
const API = location.origin + '/api/v1';
|
| 720 |
-
const AUTH = API + '/auth';
|
| 721 |
-
const DEMO = API + '/demo';
|
| 722 |
-
const ADMIN = API + '/admin';
|
| 723 |
-
const USER = API + '/user';
|
| 724 |
-
const RISK = API + '/risk';
|
| 725 |
-
|
| 726 |
-
function getToken() { return localStorage.getItem('token') || ''; }
|
| 727 |
-
function saveToken(t){ localStorage.setItem('token', t); refreshTokenBar(); }
|
| 728 |
-
function clearToken(){ localStorage.removeItem('token'); refreshTokenBar(); }
|
| 729 |
-
function copyToken() {
|
| 730 |
-
const t = getToken();
|
| 731 |
-
if (!t) { alert('No token to copy.'); return; }
|
| 732 |
-
navigator.clipboard.writeText(t).then(() => alert('Token copied!')).catch(() => alert(t.substring(0, 80) + '...'));
|
| 733 |
-
}
|
| 734 |
-
function refreshTokenBar() {
|
| 735 |
-
const t = getToken();
|
| 736 |
-
document.getElementById('tokenPreview').textContent = t ? (t.substring(0, 60) + '...') : 'No token — log in first';
|
| 737 |
-
}
|
| 738 |
-
|
| 739 |
-
async function req(url, method, body, auth) {
|
| 740 |
-
if (method === undefined) method = 'GET';
|
| 741 |
-
if (auth === undefined) auth = true;
|
| 742 |
-
const headers = { 'Content-Type': 'application/json' };
|
| 743 |
-
if (auth && getToken()) headers['Authorization'] = 'Bearer ' + getToken();
|
| 744 |
-
try {
|
| 745 |
-
const r = await fetch(url, { method: method, headers: headers, body: body ? JSON.stringify(body) : undefined });
|
| 746 |
-
const data = await r.json().catch(function() { return {}; });
|
| 747 |
-
return { ok: r.ok, status: r.status, data: data };
|
| 748 |
-
} catch(e) {
|
| 749 |
-
return { ok: false, status: 0, data: { detail: e.message } };
|
| 750 |
-
}
|
| 751 |
-
}
|
| 752 |
-
|
| 753 |
-
function showResp(id, r, extractToken) {
|
| 754 |
-
const el = document.getElementById(id);
|
| 755 |
-
el.classList.add('show');
|
| 756 |
-
if (extractToken && r.ok) {
|
| 757 |
-
const t = r.data && (r.data.access_token || (r.data.framework_decision && r.data.framework_decision.access_token));
|
| 758 |
-
if (t) saveToken(t);
|
| 759 |
-
}
|
| 760 |
-
el.style.color = r.ok ? 'var(--c-success)' : 'var(--c-danger)';
|
| 761 |
-
el.textContent = JSON.stringify(r.data, null, 2);
|
| 762 |
-
}
|
| 763 |
-
|
| 764 |
-
function setStep(prefix, step, state) {
|
| 765 |
-
if (!state) state = 'active';
|
| 766 |
-
for (var i = 0; i <= 4; i++) {
|
| 767 |
-
var el = document.getElementById(prefix + 'step' + i);
|
| 768 |
-
if (!el) continue;
|
| 769 |
-
el.classList.remove('active', 'done');
|
| 770 |
-
if (i < step) el.classList.add('done');
|
| 771 |
-
else if (i === step) el.classList.add(state);
|
| 772 |
-
}
|
| 773 |
-
}
|
| 774 |
-
|
| 775 |
-
async function ping() {
|
| 776 |
-
const dot = document.getElementById('statusDot');
|
| 777 |
-
const text = document.getElementById('statusText');
|
| 778 |
-
dot.className = 'dot'; text.textContent = 'Checking...';
|
| 779 |
-
try {
|
| 780 |
-
const r = await fetch(location.origin + '/health');
|
| 781 |
-
if (r.ok) { dot.className = 'dot online'; text.textContent = 'Server online'; }
|
| 782 |
-
else { dot.className = 'dot'; text.textContent = 'Server error (' + r.status + ')'; }
|
| 783 |
-
} catch(e) {
|
| 784 |
-
dot.className = 'dot'; text.textContent = 'Server offline';
|
| 785 |
-
}
|
| 786 |
-
}
|
| 787 |
-
|
| 788 |
-
function switchTab(id, btn) {
|
| 789 |
-
document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
|
| 790 |
-
document.querySelectorAll('.main-tabs button').forEach(function(b) { b.classList.remove('active'); });
|
| 791 |
-
document.getElementById('tab-' + id).classList.add('active');
|
| 792 |
-
btn.classList.add('active');
|
| 793 |
-
if (id === 'demo2') refreshAnomalies();
|
| 794 |
-
if (id === 'admin') { loadAdminStats(); checkEmailStatus(); }
|
| 795 |
-
if (id === 'intel') intelInitTab();
|
| 796 |
-
}
|
| 797 |
-
|
| 798 |
-
/* ---- SCENARIO 1 ---- */
|
| 799 |
-
async function setupDemo(reset) {
|
| 800 |
-
setStep('s1', 0, 'active');
|
| 801 |
-
const r = await req(DEMO + '/setup?reset=' + (reset ? 'true' : 'false'), 'POST', null, false);
|
| 802 |
-
showResp('setupResp', r);
|
| 803 |
-
if (r.ok) setStep('s1', 0, 'done');
|
| 804 |
-
}
|
| 805 |
-
|
| 806 |
-
async function checkDemoState() {
|
| 807 |
-
const r = await req(DEMO + '/state', 'GET', null, false);
|
| 808 |
-
showResp('setupResp', r);
|
| 809 |
-
}
|
| 810 |
-
|
| 811 |
-
async function doNormalLogin() {
|
| 812 |
-
setStep('s1', 1, 'active');
|
| 813 |
-
const r = await req(DEMO + '/scenario1/normal-login', 'POST', null, false);
|
| 814 |
-
showResp('normalLoginResp', r, true);
|
| 815 |
-
if (r.ok && r.data && r.data.framework_decision && r.data.framework_decision.status === 'success') {
|
| 816 |
-
setStep('s1', 1, 'done');
|
| 817 |
-
showRiskViz(r.data.framework_decision, r.data.what_the_framework_checked);
|
| 818 |
-
document.getElementById('challengeCard').style.display = 'none';
|
| 819 |
-
document.getElementById('riskVizCard').style.display = 'block';
|
| 820 |
-
}
|
| 821 |
-
}
|
| 822 |
-
|
| 823 |
-
async function doSuspiciousLogin() {
|
| 824 |
-
setStep('s1', 2, 'active');
|
| 825 |
-
const r = await req(DEMO + '/scenario1/suspicious-login', 'POST', null, false);
|
| 826 |
-
showResp('suspLoginResp', r);
|
| 827 |
-
if (r.ok && r.data) {
|
| 828 |
-
setStep('s1', 2, 'done');
|
| 829 |
-
var decision = r.data.framework_decision || {};
|
| 830 |
-
showRiskViz(decision, r.data.anomalies_triggered);
|
| 831 |
-
if (decision.status === 'challenge_required' && decision.challenge_id) {
|
| 832 |
-
document.getElementById('challengeId').value = decision.challenge_id;
|
| 833 |
-
document.getElementById('challengeCard').style.display = 'block';
|
| 834 |
-
setStep('s1', 3, 'active');
|
| 835 |
-
} else {
|
| 836 |
-
document.getElementById('challengeCard').style.display = 'none';
|
| 837 |
-
}
|
| 838 |
-
document.getElementById('riskVizCard').style.display = 'block';
|
| 839 |
-
}
|
| 840 |
-
}
|
| 841 |
-
|
| 842 |
-
async function doCompleteChallenge() {
|
| 843 |
-
var id = document.getElementById('challengeId').value.trim();
|
| 844 |
-
var code = document.getElementById('challengeCode').value.trim();
|
| 845 |
-
if (!id || !code) { alert('Enter challenge ID and code.'); return; }
|
| 846 |
-
const r = await req(DEMO + '/scenario1/complete-challenge?challenge_id=' + encodeURIComponent(id) + '&code=' + encodeURIComponent(code), 'POST', null, false);
|
| 847 |
-
showResp('challengeResp', r, true);
|
| 848 |
-
if (r.ok && r.data && r.data.result && r.data.result.status === 'success') {
|
| 849 |
-
setStep('s1', 3, 'done');
|
| 850 |
-
}
|
| 851 |
-
}
|
| 852 |
-
|
| 853 |
-
function showRiskViz(decision, notes) {
|
| 854 |
-
document.getElementById('riskVizCard').style.display = 'block';
|
| 855 |
-
var score = decision.risk_score || 0;
|
| 856 |
-
var level = decision.security_level !== undefined ? decision.security_level : 0;
|
| 857 |
-
var rl = (decision.risk_level || 'low').toLowerCase();
|
| 858 |
-
var status = decision.status || 'success';
|
| 859 |
-
var ARC_LEN = 220;
|
| 860 |
-
var offset = ARC_LEN - (score / 100) * ARC_LEN;
|
| 861 |
-
var colours = { low: '#22c55e', medium: '#f59e0b', high: '#f97316', critical: '#ef4444' };
|
| 862 |
-
var colour = colours[rl] || '#6366f1';
|
| 863 |
-
var arc = document.getElementById('gaugeArc');
|
| 864 |
-
arc.setAttribute('stroke-dashoffset', offset);
|
| 865 |
-
arc.setAttribute('stroke', colour);
|
| 866 |
-
document.getElementById('gaugeNum').textContent = Math.round(score);
|
| 867 |
-
document.getElementById('gaugeNum').style.color = colour;
|
| 868 |
-
document.getElementById('gaugeLabel').textContent = rl.toUpperCase() + ' RISK';
|
| 869 |
-
document.getElementById('gaugeLabel').style.color = colour;
|
| 870 |
-
var labels = ['active-0','active-1','active-2','active-3','active-4'];
|
| 871 |
-
for (var i = 0; i < 5; i++) {
|
| 872 |
-
var seg = document.getElementById('lseg' + i);
|
| 873 |
-
seg.className = 'level-seg';
|
| 874 |
-
if (i <= level) seg.classList.add(labels[i]);
|
| 875 |
-
}
|
| 876 |
-
var dbox = document.getElementById('decisionBox');
|
| 877 |
-
var statusLabels = { success: 'ACCESS GRANTED', challenge_required: 'CHALLENGE REQUIRED', blocked: 'BLOCKED' };
|
| 878 |
-
var statusClass = { success: 'success', challenge_required: 'challenge', blocked: 'blocked' };
|
| 879 |
-
var icons = { success: '✅', challenge_required: '⚠', blocked: '🚫' };
|
| 880 |
-
dbox.innerHTML = '<div class="decision-panel"><div class="decision-header ' + (statusClass[status] || 'success') + '">' +
|
| 881 |
-
(icons[status] || '') + ' ' + (statusLabels[status] || status.toUpperCase()) + '</div>' +
|
| 882 |
-
'<div class="decision-body" style="font-size:13px;">' +
|
| 883 |
-
'<div style="margin-bottom:6px;">Security Level: <strong>' + level + '</strong> / 4</div>' +
|
| 884 |
-
(decision.challenge_type ? '<div>Challenge: <strong>' + decision.challenge_type.toUpperCase() + '</strong></div>' : '') +
|
| 885 |
-
(decision.message ? '<div style="margin-top:6px;color:var(--c-muted);">' + decision.message + '</div>' : '') +
|
| 886 |
-
'</div></div>';
|
| 887 |
-
var factors = decision.risk_factors || {};
|
| 888 |
-
var factorEl = document.getElementById('factorBars');
|
| 889 |
-
var fkeys = ['device','location','time','velocity','behavior'];
|
| 890 |
-
var fnames = { device:'Device', location:'Location', time:'Time Pattern', velocity:'Velocity', behavior:'Behavior' };
|
| 891 |
-
var fweight = { device:'0.21%', location:'97.68%', time:'0.02%', velocity:'2.08%', behavior:'0.01%' };
|
| 892 |
-
factorEl.innerHTML = fkeys.map(function(k) {
|
| 893 |
-
var val = factors[k] || 0;
|
| 894 |
-
var fc = val > 70 ? '#ef4444' : val > 40 ? '#f59e0b' : '#22c55e';
|
| 895 |
-
return '<div class="factor-row">' +
|
| 896 |
-
'<div class="factor-label">' + fnames[k] + ' <span style="float:right;color:var(--c-muted);">' + val.toFixed(1) + ' / 100 (weight: ' + fweight[k] + ')</span></div>' +
|
| 897 |
-
'<div class="factor-bar-wrap"><div class="factor-bar" style="width:' + val + '%;background:' + fc + ';"></div></div>' +
|
| 898 |
-
'</div>';
|
| 899 |
-
}).join('');
|
| 900 |
-
if (notes && notes.length) {
|
| 901 |
-
document.getElementById('triggeredRules').innerHTML = '<div style="font-size:12px;color:var(--c-muted);font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;">What the Framework Saw</div>' +
|
| 902 |
-
notes.map(function(n) { return '<div style="font-size:13px;padding:4px 0;border-bottom:1px solid var(--c-border);">' + n + '</div>'; }).join('');
|
| 903 |
-
} else {
|
| 904 |
-
document.getElementById('triggeredRules').innerHTML = '';
|
| 905 |
-
}
|
| 906 |
-
}
|
| 907 |
-
|
| 908 |
-
/* ---- SCENARIO 2 ---- */
|
| 909 |
-
var legitResult = null;
|
| 910 |
-
var attackerResult = null;
|
| 911 |
-
|
| 912 |
-
async function doSimulateAttack() {
|
| 913 |
-
setStep('s2', 1, 'active');
|
| 914 |
-
var n = parseInt(document.getElementById('attackAttempts').value) || 12;
|
| 915 |
-
var log = document.getElementById('attackLog');
|
| 916 |
-
log.style.display = 'block';
|
| 917 |
-
log.innerHTML = '';
|
| 918 |
-
for (var i = 1; i <= n; i++) {
|
| 919 |
-
await new Promise(function(res) { setTimeout(res, 70); });
|
| 920 |
-
log.innerHTML += '<div class="line">[' + new Date().toLocaleTimeString() + '] ATTEMPT ' + i + '/' + n + ' pw:****' + i + ' → <span style="color:#f97316;">FAILED</span></div>';
|
| 921 |
-
log.scrollTop = log.scrollHeight;
|
| 922 |
-
}
|
| 923 |
-
const r = await req(DEMO + '/scenario2/simulate-attack?num_attempts=' + n, 'POST', null, false);
|
| 924 |
-
if (r.ok && r.data) {
|
| 925 |
-
var cnt = (r.data.anomalies_detected && r.data.anomalies_detected.length) || 0;
|
| 926 |
-
var aip = (r.data.attack_details && r.data.attack_details.attacker_ip) || '192.0.2.100';
|
| 927 |
-
log.innerHTML += '<div class="detected">[FRAMEWORK] AnomalyDetector fired: ' + cnt + ' pattern(s) detected.</div>';
|
| 928 |
-
log.innerHTML += '<div class="att-blocked">[FRAMEWORK] IP ' + aip + ' flagged as CRITICAL. All requests BLOCKED.</div>';
|
| 929 |
-
setStep('s2', 1, 'done');
|
| 930 |
-
}
|
| 931 |
-
showResp('attackResp', r);
|
| 932 |
-
await refreshAnomalies();
|
| 933 |
-
}
|
| 934 |
-
|
| 935 |
-
async function doLegitLogin() {
|
| 936 |
-
setStep('s2', 2, 'active');
|
| 937 |
-
const r = await req(DEMO + '/scenario2/legitimate-user', 'POST', null, false);
|
| 938 |
-
legitResult = r.data;
|
| 939 |
-
showResp('legitResp', r, true);
|
| 940 |
-
if (r.ok) setStep('s2', 2, 'done');
|
| 941 |
-
renderCompare();
|
| 942 |
-
}
|
| 943 |
-
|
| 944 |
-
async function doAttackerLogin() {
|
| 945 |
-
setStep('s2', 3, 'active');
|
| 946 |
-
const r = await req(DEMO + '/scenario2/attacker-login-attempt', 'POST', null, false);
|
| 947 |
-
attackerResult = r.data;
|
| 948 |
-
showResp('attackerLoginResp', r);
|
| 949 |
-
if (r.ok) setStep('s2', 3, 'done');
|
| 950 |
-
renderCompare();
|
| 951 |
-
}
|
| 952 |
-
|
| 953 |
-
function renderCompare() {
|
| 954 |
-
if (!legitResult && !attackerResult) return;
|
| 955 |
-
var ld = (legitResult && legitResult.framework_decision) || {};
|
| 956 |
-
var ad = (attackerResult && attackerResult.framework_decision) || {};
|
| 957 |
-
var col = function(s) { return s === 'success' ? 'var(--c-success)' : s === 'challenge_required' ? 'var(--c-warn)' : 'var(--c-danger)'; };
|
| 958 |
-
var icon = function(s) { return s === 'success' ? '✅' : s === 'challenge_required' ? '⚠️' : '🚫'; };
|
| 959 |
-
document.getElementById('compareBox').innerHTML = '<div class="compare">' +
|
| 960 |
-
'<div class="compare-side"><div class="compare-title" style="color:var(--c-success);">Legitimate User</div>' +
|
| 961 |
-
(legitResult ? '<div style="font-size:22px;text-align:center;margin:8px 0;color:' + col(ld.status) + ';">' + icon(ld.status) + '</div>' +
|
| 962 |
-
'<div style="text-align:center;font-weight:700;color:' + col(ld.status) + ';">' + (ld.status||'').toUpperCase() + '</div>' +
|
| 963 |
-
'<div style="font-size:12px;color:var(--c-muted);margin-top:6px;">Risk: ' + (ld.risk_level||'—') + ' | Level: ' + (ld.security_level !== undefined ? ld.security_level : '—') + '</div>' +
|
| 964 |
-
'<div style="font-size:12px;margin-top:4px;">' + (legitResult.key_insight||'') + '</div>'
|
| 965 |
-
: '<p style="color:var(--c-muted);">Run step 2</p>') + '</div>' +
|
| 966 |
-
'<div class="compare-side"><div class="compare-title" style="color:var(--c-danger);">Attacker (correct password)</div>' +
|
| 967 |
-
(attackerResult ? '<div style="font-size:22px;text-align:center;margin:8px 0;color:' + col(ad.status) + ';">' + icon(ad.status) + '</div>' +
|
| 968 |
-
'<div style="text-align:center;font-weight:700;color:' + col(ad.status) + ';">' + (ad.status||'').toUpperCase() + '</div>' +
|
| 969 |
-
'<div style="font-size:12px;color:var(--c-muted);margin-top:6px;">Risk: ' + (ad.risk_level||'—') + ' | Level: ' + (ad.security_level !== undefined ? ad.security_level : '—') + '</div>' +
|
| 970 |
-
'<div style="font-size:12px;margin-top:4px;">' + (attackerResult.key_insight||'') + '</div>'
|
| 971 |
-
: '<p style="color:var(--c-muted);">Run step 3</p>') + '</div>' +
|
| 972 |
-
'</div>';
|
| 973 |
-
}
|
| 974 |
-
|
| 975 |
-
async function refreshAnomalies() {
|
| 976 |
-
const r = await req(DEMO + '/scenario2/anomalies', 'GET', null, false);
|
| 977 |
-
var feed = document.getElementById('anomalyFeed');
|
| 978 |
-
if (!r.ok || !r.data || !r.data.active_anomalies || !r.data.active_anomalies.length) {
|
| 979 |
-
feed.innerHTML = '<p style="color:var(--c-muted);font-size:13px;">No active anomalies.</p>';
|
| 980 |
-
return;
|
| 981 |
-
}
|
| 982 |
-
feed.innerHTML = r.data.active_anomalies.map(function(a) {
|
| 983 |
-
return '<div class="anomaly-item"><div style="font-size:22px;">🚨</div><div style="flex:1;">' +
|
| 984 |
-
'<div class="anomaly-type">' + a.type + '</div>' +
|
| 985 |
-
'<div class="anomaly-meta">IP: <code>' + (a.ip||'—') + '</code> | Confidence: <strong>' + (a.confidence||'—') + '</strong>' +
|
| 986 |
-
' | <span class="badge badge-blocked">' + (a.severity||'—') + '</span>' +
|
| 987 |
-
' | ' + new Date(a.first_detected).toLocaleTimeString() + '</div></div></div>';
|
| 988 |
-
}).join('');
|
| 989 |
-
}
|
| 990 |
-
|
| 991 |
-
async function clearAnomalies() {
|
| 992 |
-
await req(DEMO + '/scenario2/clear-anomalies', 'DELETE', null, false);
|
| 993 |
-
await refreshAnomalies();
|
| 994 |
-
monitorLog('info', 'All anomalies cleared.');
|
| 995 |
-
}
|
| 996 |
-
|
| 997 |
-
/* ---- CONTINUOUS MONITORING ---- */
|
| 998 |
-
var monitorInterval = null;
|
| 999 |
-
var monitorCycleCount = 0;
|
| 1000 |
-
|
| 1001 |
-
function toggleMonitoring() {
|
| 1002 |
-
if (monitorInterval) { stopMonitoring(); } else { startMonitoring(); }
|
| 1003 |
-
}
|
| 1004 |
-
|
| 1005 |
-
function startMonitoring() {
|
| 1006 |
-
var btn = document.getElementById('monitorBtn');
|
| 1007 |
-
var badge = document.getElementById('monitorBadge');
|
| 1008 |
-
document.getElementById('monitorStats').style.display = 'flex';
|
| 1009 |
-
document.getElementById('monitorLog').style.display = 'block';
|
| 1010 |
-
btn.textContent = '\u23F9 Stop Monitoring';
|
| 1011 |
-
btn.className = 'btn btn-danger btn-sm';
|
| 1012 |
-
badge.className = 'monitor-badge mon-on monitor-pulse';
|
| 1013 |
-
badge.innerHTML = '● Live';
|
| 1014 |
-
monitorCycleCount = 0;
|
| 1015 |
-
monitorLog('info', 'Continuous monitoring started. Scanning every 2 s...');
|
| 1016 |
-
runMonitoringCycle();
|
| 1017 |
-
monitorInterval = setInterval(runMonitoringCycle, 2000);
|
| 1018 |
-
}
|
| 1019 |
-
|
| 1020 |
-
function stopMonitoring() {
|
| 1021 |
-
clearInterval(monitorInterval);
|
| 1022 |
-
monitorInterval = null;
|
| 1023 |
-
var btn = document.getElementById('monitorBtn');
|
| 1024 |
-
var badge = document.getElementById('monitorBadge');
|
| 1025 |
-
btn.textContent = '\u25B6 Start Monitoring';
|
| 1026 |
-
btn.className = 'btn btn-success btn-sm';
|
| 1027 |
-
badge.className = 'monitor-badge mon-off';
|
| 1028 |
-
badge.innerHTML = '⏸ Idle';
|
| 1029 |
-
monitorLog('info', 'Monitoring stopped after ' + monitorCycleCount + ' cycles.');
|
| 1030 |
-
}
|
| 1031 |
-
|
| 1032 |
-
async function runMonitoringCycle() {
|
| 1033 |
-
monitorCycleCount++;
|
| 1034 |
-
var r = await req(DEMO + '/scenario2/run-monitoring-cycle', 'POST', null, false);
|
| 1035 |
-
if (!r.ok) {
|
| 1036 |
-
monitorLog('warn', 'Cycle ' + monitorCycleCount + ' failed: ' + (r.data && r.data.detail ? r.data.detail : 'server error'));
|
| 1037 |
-
return;
|
| 1038 |
-
}
|
| 1039 |
-
var d = r.data;
|
| 1040 |
-
var cycleTime = new Date(d.cycle_at).toLocaleTimeString();
|
| 1041 |
-
|
| 1042 |
-
// Update stats strip
|
| 1043 |
-
var threatEl = document.getElementById('msThreatVal');
|
| 1044 |
-
var threat = d.threat_level || 'NORMAL';
|
| 1045 |
-
threatEl.textContent = threat;
|
| 1046 |
-
threatEl.style.color = threat === 'NORMAL' ? 'var(--c-success)' : threat === 'CRITICAL' ? 'var(--c-danger)' : 'var(--c-warn)';
|
| 1047 |
-
document.getElementById('msAnomalyVal').textContent = d.scan.total_active_anomalies;
|
| 1048 |
-
document.getElementById('msFailedVal').textContent = d.scan.recent_failed_logins_1h;
|
| 1049 |
-
document.getElementById('msSessionVal').textContent = d.sessions.active;
|
| 1050 |
-
document.getElementById('msSuspVal').textContent = d.sessions.suspicious;
|
| 1051 |
-
document.getElementById('msCycleVal').textContent = cycleTime;
|
| 1052 |
-
|
| 1053 |
-
// Monitoring log line
|
| 1054 |
-
var anomCnt = d.scan.total_active_anomalies;
|
| 1055 |
-
var cls = anomCnt > 0 ? 'ml-threat' : 'ml-ok';
|
| 1056 |
-
var icon = anomCnt > 0 ? '\u26A0' : '\u2713';
|
| 1057 |
-
var bfFlag = d.scan.brute_force_active ? ' | BruteForce:ACTIVE' : '';
|
| 1058 |
-
var csFlag = d.scan.credential_stuffing_active ? ' | CredStuffing:ACTIVE' : '';
|
| 1059 |
-
monitorLog(anomCnt > 0 ? 'threat' : 'ok',
|
| 1060 |
-
'[' + cycleTime + '] #' + monitorCycleCount + ' ' + icon +
|
| 1061 |
-
' Threat:' + threat +
|
| 1062 |
-
' Anomalies:' + anomCnt +
|
| 1063 |
-
' Failed/1h:' + d.scan.recent_failed_logins_1h +
|
| 1064 |
-
bfFlag + csFlag
|
| 1065 |
-
);
|
| 1066 |
-
|
| 1067 |
-
// Also refresh the visual anomaly feed every 3rd cycle
|
| 1068 |
-
if (monitorCycleCount % 3 === 0) { await refreshAnomalies(); }
|
| 1069 |
-
}
|
| 1070 |
-
|
| 1071 |
-
function monitorLog(type, msg) {
|
| 1072 |
-
var log = document.getElementById('monitorLog');
|
| 1073 |
-
if (!log) return;
|
| 1074 |
-
var cls = { ok: 'ml-ok', threat: 'ml-threat', warn: 'ml-warn', info: 'ml-info' }[type] || 'ml-info';
|
| 1075 |
-
var line = document.createElement('div');
|
| 1076 |
-
line.className = cls;
|
| 1077 |
-
line.textContent = msg;
|
| 1078 |
-
log.appendChild(line);
|
| 1079 |
-
log.scrollTop = log.scrollHeight;
|
| 1080 |
-
// Keep log at max 200 lines
|
| 1081 |
-
while (log.childNodes.length > 200) { log.removeChild(log.firstChild); }
|
| 1082 |
-
}
|
| 1083 |
-
|
| 1084 |
-
/* ---- API TESTING ---- */
|
| 1085 |
-
async function apiRegister() {
|
| 1086 |
-
var body = { email: document.getElementById('regEmail').value, password: document.getElementById('regPassword').value };
|
| 1087 |
-
var name = document.getElementById('regName').value;
|
| 1088 |
-
if (name) body.full_name = name;
|
| 1089 |
-
if (!body.email || !body.password) { alert('Email and password required.'); return; }
|
| 1090 |
-
showResp('regResp', await req(AUTH + '/register', 'POST', body, false));
|
| 1091 |
-
}
|
| 1092 |
-
|
| 1093 |
-
async function apiLogin() {
|
| 1094 |
-
var body = { email: document.getElementById('loginEmail').value, password: document.getElementById('loginPassword').value };
|
| 1095 |
-
if (!body.email || !body.password) { alert('Email and password required.'); return; }
|
| 1096 |
-
showResp('loginResp', await req(AUTH + '/login', 'POST', body, false), true);
|
| 1097 |
-
}
|
| 1098 |
-
|
| 1099 |
-
async function apiAdaptiveLogin() {
|
| 1100 |
-
var body = { email: document.getElementById('loginEmail').value, password: document.getElementById('loginPassword').value };
|
| 1101 |
-
if (!body.email || !body.password) { alert('Email and password required.'); return; }
|
| 1102 |
-
showResp('loginResp', await req(AUTH + '/adaptive-login', 'POST', body, false), true);
|
| 1103 |
-
}
|
| 1104 |
-
|
| 1105 |
-
async function api2faEnable() {
|
| 1106 |
-
const r = await req(AUTH + '/enable-2fa', 'POST');
|
| 1107 |
-
if (r.ok && r.data && r.data.qr_code) {
|
| 1108 |
-
document.getElementById('qrWrap').innerHTML = '<img src="' + r.data.qr_code + '" style="max-width:180px;border:2px solid var(--c-border);border-radius:8px;">';
|
| 1109 |
-
}
|
| 1110 |
-
showResp('tfaResp', r);
|
| 1111 |
-
}
|
| 1112 |
-
|
| 1113 |
-
async function api2faVerify() {
|
| 1114 |
-
var code = document.getElementById('totpCode').value.trim();
|
| 1115 |
-
if (!code) { alert('Enter TOTP code.'); return; }
|
| 1116 |
-
showResp('tfaResp', await req(AUTH + '/verify-2fa', 'POST', { otp: code }));
|
| 1117 |
-
}
|
| 1118 |
-
|
| 1119 |
-
async function api2faDisable() {
|
| 1120 |
-
var pwd = prompt('Enter your password to disable 2FA:');
|
| 1121 |
-
if (!pwd) return;
|
| 1122 |
-
showResp('tfaResp', await req(AUTH + '/disable-2fa?password=' + encodeURIComponent(pwd), 'POST'));
|
| 1123 |
-
}
|
| 1124 |
-
|
| 1125 |
-
async function api2faStatus() { showResp('tfaResp', await req(USER + '/security')); }
|
| 1126 |
-
|
| 1127 |
-
async function apiGetProfile() { showResp('profileResp', await req(USER + '/profile')); }
|
| 1128 |
-
async function apiGetSecurity() { showResp('profileResp', await req(USER + '/security')); }
|
| 1129 |
-
async function apiGetDevices() { showResp('profileResp', await req(USER + '/devices')); }
|
| 1130 |
-
async function apiGetSessions() { showResp('profileResp', await req(USER + '/sessions')); }
|
| 1131 |
-
async function apiRevokeAll() { showResp('profileResp', await req(USER + '/sessions/revoke', 'POST', { session_ids: [], revoke_all: true })); }
|
| 1132 |
-
async function apiRiskProfile() { showResp('protResp', await req(USER + '/risk-profile')); }
|
| 1133 |
-
|
| 1134 |
-
async function apiChangePwd() {
|
| 1135 |
-
var cur = document.getElementById('curPwd').value;
|
| 1136 |
-
var nw = document.getElementById('newPwd').value;
|
| 1137 |
-
showResp('pwdResp', await req(USER + '/change-password', 'POST', { current_password: cur, new_password: nw, confirm_password: nw }));
|
| 1138 |
-
}
|
| 1139 |
-
|
| 1140 |
-
async function apiForgotPwd() {
|
| 1141 |
-
var email = document.getElementById('resetEmail').value;
|
| 1142 |
-
if (!email) { alert('Enter email.'); return; }
|
| 1143 |
-
showResp('pwdResp', await req(AUTH + '/forgot-password', 'POST', { email: email }, false));
|
| 1144 |
-
}
|
| 1145 |
-
|
| 1146 |
-
async function apiProtected() { showResp('protResp', await req(API + '/protected')); }
|
| 1147 |
-
async function apiAdminOnly() { showResp('protResp', await req(API + '/admin-only')); }
|
| 1148 |
-
async function apiLogout() { const r = await req(AUTH + '/logout', 'POST'); clearToken(); showResp('protResp', r); }
|
| 1149 |
-
|
| 1150 |
-
/* ═══════════════════════════════════════════════════════════
|
| 1151 |
-
SESSION INTELLIGENCE TAB — 8 Advanced Security Features
|
| 1152 |
-
═══════════════════════════════════════════════════════════ */
|
| 1153 |
-
const INTEL = API + '/session-intel';
|
| 1154 |
-
|
| 1155 |
-
// ── Behavior collection state ─────────────────────────────
|
| 1156 |
-
var _bhCollecting = false;
|
| 1157 |
-
var _bhKeyTimes = [];
|
| 1158 |
-
var _bhMousePts = [];
|
| 1159 |
-
var _bhScrollDs = [];
|
| 1160 |
-
var _bhLastKey = 0;
|
| 1161 |
-
var _bhLastMouse = 0;
|
| 1162 |
-
var _currentChallengeId = null;
|
| 1163 |
-
|
| 1164 |
-
function toggleBehaviorCollector() {
|
| 1165 |
-
if (_bhCollecting) { stopBehaviorCollector(); } else { startBehaviorCollector(); }
|
| 1166 |
-
}
|
| 1167 |
-
function startBehaviorCollector() {
|
| 1168 |
-
_bhCollecting = true;
|
| 1169 |
-
_bhKeyTimes = []; _bhMousePts = []; _bhScrollDs = []; _bhLastKey = 0; _bhLastMouse = 0;
|
| 1170 |
-
document.addEventListener('keydown', _bhOnKey);
|
| 1171 |
-
document.addEventListener('mousemove', _bhOnMouse);
|
| 1172 |
-
document.addEventListener('scroll', _bhOnScroll, true);
|
| 1173 |
-
var btn = document.getElementById('behaviorCollectBtn');
|
| 1174 |
-
btn.textContent = '\u23F9 Stop Collecting';
|
| 1175 |
-
btn.className = 'btn btn-danger btn-sm';
|
| 1176 |
-
document.getElementById('collectStatus').textContent = 'Collecting\u2026 (type, move mouse, scroll)';
|
| 1177 |
-
_bhRenderBars(0.5, 0.5, 0.5);
|
| 1178 |
-
}
|
| 1179 |
-
function stopBehaviorCollector() {
|
| 1180 |
-
_bhCollecting = false;
|
| 1181 |
-
document.removeEventListener('keydown', _bhOnKey);
|
| 1182 |
-
document.removeEventListener('mousemove', _bhOnMouse);
|
| 1183 |
-
document.removeEventListener('scroll', _bhOnScroll, true);
|
| 1184 |
-
var btn = document.getElementById('behaviorCollectBtn');
|
| 1185 |
-
btn.textContent = '\u25B6 Start Collecting';
|
| 1186 |
-
btn.className = 'btn btn-success btn-sm';
|
| 1187 |
-
var s = _bhComputeScores();
|
| 1188 |
-
_bhRenderBars(s.te, s.ml, s.sv);
|
| 1189 |
-
document.getElementById('collectStatus').textContent =
|
| 1190 |
-
'Done \u2014 Keys:' + _bhKeyTimes.length + ' MousePts:' + _bhMousePts.length + ' Scrolls:' + _bhScrollDs.length;
|
| 1191 |
-
}
|
| 1192 |
-
function _bhOnKey(e) {
|
| 1193 |
-
var now = performance.now();
|
| 1194 |
-
if (_bhLastKey > 0) _bhKeyTimes.push(now - _bhLastKey);
|
| 1195 |
-
_bhLastKey = now;
|
| 1196 |
-
}
|
| 1197 |
-
function _bhOnMouse(e) {
|
| 1198 |
-
var now = performance.now();
|
| 1199 |
-
if (now - _bhLastMouse < 50) return;
|
| 1200 |
-
_bhLastMouse = now;
|
| 1201 |
-
_bhMousePts.push([e.clientX, e.clientY]);
|
| 1202 |
-
}
|
| 1203 |
-
function _bhOnScroll(e) {
|
| 1204 |
-
var delta = e.target && e.target.scrollTop !== undefined ? Math.abs(e.target.scrollTop) : window.scrollY;
|
| 1205 |
-
_bhScrollDs.push(delta);
|
| 1206 |
-
}
|
| 1207 |
-
function _bhComputeScores() {
|
| 1208 |
-
// 1. Typing entropy — coefficient of variation of inter-key delays
|
| 1209 |
-
var te = 0.5;
|
| 1210 |
-
if (_bhKeyTimes.length >= 3) {
|
| 1211 |
-
var mean = _bhKeyTimes.reduce(function(a,b){return a+b;},0) / _bhKeyTimes.length;
|
| 1212 |
-
var std = Math.sqrt(_bhKeyTimes.reduce(function(s,v){return s+(v-mean)*(v-mean);},0) / _bhKeyTimes.length);
|
| 1213 |
-
var cv = std / (mean + 1e-6);
|
| 1214 |
-
te = cv < 0.05 ? 0.10 : cv < 0.20 ? 0.30 + cv*1.5 : cv < 0.90 ? 0.50 + (cv-0.20)*0.7 : Math.max(0.10, 1.0-(cv-0.90)*0.8);
|
| 1215 |
-
te = Math.max(0, Math.min(1, te));
|
| 1216 |
-
}
|
| 1217 |
-
// 2. Mouse linearity — straight/curved path ratio
|
| 1218 |
-
var ml = 0.6;
|
| 1219 |
-
if (_bhMousePts.length >= 3) {
|
| 1220 |
-
var totalD = 0;
|
| 1221 |
-
for (var i=1;i<_bhMousePts.length;i++){
|
| 1222 |
-
var dx=_bhMousePts[i][0]-_bhMousePts[i-1][0], dy=_bhMousePts[i][1]-_bhMousePts[i-1][1];
|
| 1223 |
-
totalD += Math.sqrt(dx*dx+dy*dy);
|
| 1224 |
-
}
|
| 1225 |
-
var dxA=_bhMousePts[_bhMousePts.length-1][0]-_bhMousePts[0][0];
|
| 1226 |
-
var dyA=_bhMousePts[_bhMousePts.length-1][1]-_bhMousePts[0][1];
|
| 1227 |
-
var straightD = Math.sqrt(dxA*dxA+dyA*dyA);
|
| 1228 |
-
var ratio = totalD > 0 ? straightD/totalD : 0;
|
| 1229 |
-
ml = Math.max(0, Math.min(1, 1.0 - Math.abs(ratio-0.6)*1.5));
|
| 1230 |
-
}
|
| 1231 |
-
// 3. Scroll variance — normalised std of scroll deltas
|
| 1232 |
-
var sv = 0.5;
|
| 1233 |
-
if (_bhScrollDs.length >= 2) {
|
| 1234 |
-
var sm = _bhScrollDs.reduce(function(a,b){return a+b;},0)/_bhScrollDs.length;
|
| 1235 |
-
var ss = Math.sqrt(_bhScrollDs.reduce(function(s,v){return s+Math.pow(v-sm,2);},0)/_bhScrollDs.length);
|
| 1236 |
-
sv = Math.min(1, ss/200);
|
| 1237 |
-
}
|
| 1238 |
-
var lr = Math.max(0, 1.0 - (0.40*te + 0.35*ml + 0.25*sv));
|
| 1239 |
-
return { te:te, ml:ml, sv:sv, lr:lr };
|
| 1240 |
-
}
|
| 1241 |
-
function _bhRenderBars(te, ml, sv) {
|
| 1242 |
-
function bCol(v){ return v>0.7?'#22c55e':v>0.4?'#f59e0b':'#ef4444'; }
|
| 1243 |
-
document.getElementById('teVal').textContent = te.toFixed(2);
|
| 1244 |
-
document.getElementById('mlVal').textContent = ml.toFixed(2);
|
| 1245 |
-
document.getElementById('svVal').textContent = sv.toFixed(2);
|
| 1246 |
-
var teEl = document.getElementById('teBar'); teEl.style.width = (te*100)+'%'; teEl.style.background = bCol(te);
|
| 1247 |
-
var mlEl = document.getElementById('mlBar'); mlEl.style.width = (ml*100)+'%'; mlEl.style.background = bCol(ml);
|
| 1248 |
-
var svEl = document.getElementById('svBar'); svEl.style.width = (sv*100)+'%'; svEl.style.background = bCol(sv);
|
| 1249 |
-
}
|
| 1250 |
-
|
| 1251 |
-
// ── Trust gauge ───────────────────────────────────────────
|
| 1252 |
-
function updateTrustGauge(score, label, color) {
|
| 1253 |
-
var ARC = 204;
|
| 1254 |
-
var offset = ARC - (score/100)*ARC;
|
| 1255 |
-
var arc = document.getElementById('trustArc');
|
| 1256 |
-
arc.setAttribute('stroke-dashoffset', offset);
|
| 1257 |
-
arc.setAttribute('stroke', color || '#22c55e');
|
| 1258 |
-
document.getElementById('trustNum').textContent = Math.round(score);
|
| 1259 |
-
document.getElementById('trustNum').setAttribute('fill', color || '#f1f5f9');
|
| 1260 |
-
document.getElementById('trustLabel').textContent = (label||'trusted').toUpperCase();
|
| 1261 |
-
document.getElementById('trustLabel').style.color = color || 'var(--c-muted)';
|
| 1262 |
-
}
|
| 1263 |
-
function renderTrustHistory(events) {
|
| 1264 |
-
var wrap = document.getElementById('trustHistoryWrap');
|
| 1265 |
-
if (!events || !events.length) { wrap.innerHTML = '<p style="color:var(--c-muted);">No events yet.</p>'; return; }
|
| 1266 |
-
wrap.innerHTML = '<div style="color:var(--c-muted);font-size:10px;text-transform:uppercase;font-weight:700;margin-bottom:4px;">Recent Trust Events</div>' +
|
| 1267 |
-
events.slice(-20).reverse().map(function(e){
|
| 1268 |
-
var sign = e.delta>=0?'+':'';
|
| 1269 |
-
var col = e.delta>=0?'var(--c-success)':'var(--c-danger)';
|
| 1270 |
-
return '<div style="display:flex;justify-content:space-between;border-bottom:1px solid var(--c-border);padding:2px 0;font-size:11px;">' +
|
| 1271 |
-
'<span style="color:var(--c-muted);">' + e.event_type + '</span>' +
|
| 1272 |
-
'<span style="color:'+col+';">' + sign + e.delta.toFixed(1) + ' \u2192 ' + e.score.toFixed(0) + '</span></div>';
|
| 1273 |
-
}).join('');
|
| 1274 |
-
}
|
| 1275 |
-
|
| 1276 |
-
async function intelGetTrust() {
|
| 1277 |
-
if (!getToken()) { alert('Login first.'); return; }
|
| 1278 |
-
var r = await req(INTEL + '/trust-score');
|
| 1279 |
-
showResp('trustResp', r);
|
| 1280 |
-
if (r.ok && r.data) {
|
| 1281 |
-
updateTrustGauge(r.data.trust_score, r.data.label, r.data.color);
|
| 1282 |
-
renderTrustHistory(r.data.history || []);
|
| 1283 |
-
}
|
| 1284 |
-
}
|
| 1285 |
-
async function intelContinuousVerify() {
|
| 1286 |
-
if (!getToken()) { alert('Login first.'); return; }
|
| 1287 |
-
var r = await req(INTEL + '/continuous-verify', 'POST', {});
|
| 1288 |
-
showResp('trustResp', r);
|
| 1289 |
-
if (r.ok && r.data) updateTrustGauge(r.data.trust_score, r.data.label, r.data.color);
|
| 1290 |
-
}
|
| 1291 |
-
async function intelDropTrust() {
|
| 1292 |
-
if (!getToken()) { alert('Login first (use Quick Login above).'); return; }
|
| 1293 |
-
var r = await req(INTEL + '/simulate-trust-drop', 'POST', { target_score: 25, reason: 'Manual demo drop' });
|
| 1294 |
-
showResp('trustResp', r);
|
| 1295 |
-
if (r.ok && r.data) {
|
| 1296 |
-
updateTrustGauge(r.data.new_trust, r.data.trust_label, r.data.trust_color);
|
| 1297 |
-
if (r.data.challenge_recommended) {
|
| 1298 |
-
document.getElementById('trustResp').textContent +=
|
| 1299 |
-
'\n\n\u26A0 Trust is now ' + r.data.new_trust.toFixed(0) + '/100 \u2014 scroll to Micro-Challenges and click Generate!';
|
| 1300 |
-
}
|
| 1301 |
-
}
|
| 1302 |
-
}
|
| 1303 |
-
|
| 1304 |
-
// ── Behavior Signals ──────────────────────────────────────
|
| 1305 |
-
async function intelSendBehavior() {
|
| 1306 |
-
if (!getToken()) { alert('Login first.'); return; }
|
| 1307 |
-
var s = _bhComputeScores();
|
| 1308 |
-
_bhRenderBars(s.te, s.ml, s.sv);
|
| 1309 |
-
var r = await req(INTEL + '/behavior-signal', 'POST', {
|
| 1310 |
-
typing_entropy: s.te,
|
| 1311 |
-
mouse_linearity: s.ml,
|
| 1312 |
-
scroll_variance: s.sv,
|
| 1313 |
-
local_risk_score: s.lr,
|
| 1314 |
-
});
|
| 1315 |
-
showResp('behaviorResp', r);
|
| 1316 |
-
if (r.ok && r.data && r.data.trust) {
|
| 1317 |
-
updateTrustGauge(r.data.trust.score, r.data.trust.label, r.data.trust.color);
|
| 1318 |
-
if (r.data.micro_challenge_recommended) {
|
| 1319 |
-
document.getElementById('behaviorResp').textContent +=
|
| 1320 |
-
'\n\n\u26A0 Trust below 40 \u2014 micro-challenge recommended!';
|
| 1321 |
-
}
|
| 1322 |
-
}
|
| 1323 |
-
}
|
| 1324 |
-
|
| 1325 |
-
// ── Impossible Travel (public demo) ──────────────────────
|
| 1326 |
-
async function intelLoadCities() {
|
| 1327 |
-
var r = await req(INTEL + '/demo/city-list', 'GET', null, false);
|
| 1328 |
-
if (!r.ok || !r.data) return;
|
| 1329 |
-
var cities = r.data.cities || [];
|
| 1330 |
-
['travelFrom','travelTo'].forEach(function(id){
|
| 1331 |
-
var sel = document.getElementById(id);
|
| 1332 |
-
sel.innerHTML = '';
|
| 1333 |
-
cities.forEach(function(c){
|
| 1334 |
-
var opt = document.createElement('option');
|
| 1335 |
-
opt.value = c.name;
|
| 1336 |
-
opt.textContent = c.name;
|
| 1337 |
-
sel.appendChild(opt);
|
| 1338 |
-
});
|
| 1339 |
-
if (id === 'travelFrom') sel.value = 'New York';
|
| 1340 |
-
if (id === 'travelTo') sel.value = 'Moscow';
|
| 1341 |
-
});
|
| 1342 |
-
}
|
| 1343 |
-
async function intelCheckTravel() {
|
| 1344 |
-
var fromCity = document.getElementById('travelFrom').value;
|
| 1345 |
-
var toCity = document.getElementById('travelTo').value;
|
| 1346 |
-
var hours = parseFloat(document.getElementById('travelHours').value);
|
| 1347 |
-
var r = await req(INTEL + '/demo/impossible-travel', 'POST',
|
| 1348 |
-
{ from_city:fromCity, to_city:toCity, time_gap_hours:hours, from_country:'', to_country:'' }, false);
|
| 1349 |
-
var el = document.getElementById('travelResult');
|
| 1350 |
-
if (!r.ok || !r.data) { el.textContent = 'Error: '+ JSON.stringify(r.data); return; }
|
| 1351 |
-
var d = r.data;
|
| 1352 |
-
var cmap = { impossible:'#ef4444', suspicious:'#f97316', plausible:'#22c55e', same_area:'#22c55e', coords_unknown:'#94a3b8' };
|
| 1353 |
-
var imap = { impossible:'\uD83D\uDEA8', suspicious:'\u26A0\uFE0F', plausible:'\u2705', same_area:'\u2705', coords_unknown:'\u2753' };
|
| 1354 |
-
var col = cmap[d.verdict]||'#94a3b8', icon = imap[d.verdict]||'\u2753';
|
| 1355 |
-
el.innerHTML = '<div style="background:'+col+'22;border:1px solid '+col+';border-radius:8px;padding:10px;">' +
|
| 1356 |
-
'<div style="font-weight:700;font-size:15px;color:'+col+';margin-bottom:6px;">' + icon + ' ' + (d.verdict||'').toUpperCase().replace('_',' ') + '</div>' +
|
| 1357 |
-
'<div style="font-size:13px;">' + d.message + '</div>' +
|
| 1358 |
-
'<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;margin-top:8px;font-size:12px;">' +
|
| 1359 |
-
'<div><strong>' + (d.distance_km||0) + ' km</strong><br><span style="color:var(--c-muted);">Distance</span></div>' +
|
| 1360 |
-
'<div><strong>' + Math.round(d.speed_kmh||0) + ' km/h</strong><br><span style="color:var(--c-muted);">Speed</span></div>' +
|
| 1361 |
-
'<div><strong>' + Math.round(d.time_gap_minutes||0) + ' min</strong><br><span style="color:var(--c-muted);">Gap</span></div>' +
|
| 1362 |
-
'</div>' +
|
| 1363 |
-
(d.trust_delta < 0 ? '<div style="color:#f97316;font-size:12px;margin-top:6px;">\u26A0 Trust impact: ' + d.trust_delta + ' pts</div>' : '') +
|
| 1364 |
-
'</div>';
|
| 1365 |
-
}
|
| 1366 |
-
|
| 1367 |
-
// ── AI Anomaly Scorer (public demo) ──────────────────────
|
| 1368 |
-
async function intelScoreAnomaly() {
|
| 1369 |
-
var r = await req(INTEL + '/demo/anomaly-score', 'POST', {
|
| 1370 |
-
typing_entropy: parseFloat(document.getElementById('aiTyping').value),
|
| 1371 |
-
mouse_linearity: parseFloat(document.getElementById('aiMouse').value),
|
| 1372 |
-
scroll_variance: parseFloat(document.getElementById('aiScroll').value),
|
| 1373 |
-
hour_normalized: parseFloat(document.getElementById('aiHour').value),
|
| 1374 |
-
failed_attempts_norm: parseFloat(document.getElementById('aiFailed').value),
|
| 1375 |
-
}, false);
|
| 1376 |
-
var el = document.getElementById('anomalyResult');
|
| 1377 |
-
if (!r.ok || !r.data) { el.textContent = JSON.stringify(r.data); return; }
|
| 1378 |
-
var d = r.data;
|
| 1379 |
-
el.innerHTML =
|
| 1380 |
-
'<div style="background:'+d.color+'22;border:1px solid '+d.color+';border-radius:8px;padding:10px;">' +
|
| 1381 |
-
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">' +
|
| 1382 |
-
'<div style="font-weight:700;font-size:20px;color:'+d.color+';">' + d.anomaly_score.toFixed(1) + ' / 100</div>' +
|
| 1383 |
-
'<div style="font-size:12px;font-weight:700;background:'+d.color+';color:#0f172a;padding:2px 10px;border-radius:4px;">' + d.classification + '</div>' +
|
| 1384 |
-
'</div>' +
|
| 1385 |
-
'<div style="font-size:11px;color:var(--c-muted);margin-bottom:8px;">Confidence: '+(d.confidence*100).toFixed(0)+'% \u00B7 Statistical Isolation Forest</div>' +
|
| 1386 |
-
Object.entries(d.per_feature||{}).map(function(kv){
|
| 1387 |
-
var fn=kv[0], fs=kv[1], fc=fs>60?'#ef4444':fs>30?'#f59e0b':'#22c55e';
|
| 1388 |
-
return '<div style="margin-bottom:5px;"><div style="display:flex;justify-content:space-between;font-size:11px;">' +
|
| 1389 |
-
'<span>'+fn+'</span><span style="color:'+fc+';">'+fs.toFixed(1)+'</span></div>' +
|
| 1390 |
-
'<div style="height:5px;background:#1e293b;border-radius:2px;overflow:hidden;">' +
|
| 1391 |
-
'<div style="height:100%;width:'+fs+'%;background:'+fc+';border-radius:2px;"></div></div></div>';
|
| 1392 |
-
}).join('') + '</div>';
|
| 1393 |
-
}
|
| 1394 |
-
|
| 1395 |
-
// ── Micro-Challenges ──────────────────────────────────────
|
| 1396 |
-
async function intelGenerateChallenge() {
|
| 1397 |
-
if (!getToken()) {
|
| 1398 |
-
// Demo mode without DB persistence
|
| 1399 |
-
var a = Math.floor(Math.random()*9)+2, b = Math.floor(Math.random()*9)+2;
|
| 1400 |
-
_currentChallengeId = 'demo-no-auth';
|
| 1401 |
-
document.getElementById('challengePanel').style.display = 'block';
|
| 1402 |
-
document.getElementById('challengeQuestion').textContent = 'What is ' + a + ' \u00D7 ' + b + ' ?';
|
| 1403 |
-
document.getElementById('challengeAnswer').value = '';
|
| 1404 |
-
document.getElementById('challengeResultMsg').textContent = '(Demo mode \u2014 login to persist trust changes)';
|
| 1405 |
-
return;
|
| 1406 |
-
}
|
| 1407 |
-
var r = await req(INTEL + '/micro-challenge/generate', 'POST', {});
|
| 1408 |
-
showResp('challengeResp', r);
|
| 1409 |
-
if (r.ok && r.data && r.data.challenge) {
|
| 1410 |
-
_currentChallengeId = r.data.challenge.challenge_id;
|
| 1411 |
-
document.getElementById('challengePanel').style.display = 'block';
|
| 1412 |
-
document.getElementById('challengeQuestion').textContent = r.data.challenge.question;
|
| 1413 |
-
document.getElementById('challengeAnswer').value = '';
|
| 1414 |
-
document.getElementById('challengeResultMsg').textContent = '';
|
| 1415 |
-
if (!r.data.challenge_needed) {
|
| 1416 |
-
document.getElementById('challengeResultMsg').textContent =
|
| 1417 |
-
'\u2139\uFE0F Trust is healthy \u2014 showing challenge anyway for demo purposes.';
|
| 1418 |
-
}
|
| 1419 |
-
}
|
| 1420 |
-
}
|
| 1421 |
-
async function intelVerifyChallenge() {
|
| 1422 |
-
var answer = document.getElementById('challengeAnswer').value.trim();
|
| 1423 |
-
if (!answer) { alert('Enter your answer.'); return; }
|
| 1424 |
-
if (!_currentChallengeId || _currentChallengeId === 'demo-no-auth') {
|
| 1425 |
-
document.getElementById('challengeResultMsg').textContent = '\u2705 Submitted (login to update real trust score).';
|
| 1426 |
-
document.getElementById('challengePanel').style.display = 'none';
|
| 1427 |
-
return;
|
| 1428 |
-
}
|
| 1429 |
-
var r = await req(INTEL + '/micro-challenge/verify', 'POST', { challenge_id:_currentChallengeId, response:answer });
|
| 1430 |
-
showResp('challengeResp', r);
|
| 1431 |
-
if (r.ok && r.data) {
|
| 1432 |
-
document.getElementById('challengeResultMsg').textContent = r.data.reason;
|
| 1433 |
-
if (r.data.correct) {
|
| 1434 |
-
document.getElementById('challengePanel').style.display = 'none';
|
| 1435 |
-
updateTrustGauge(r.data.new_trust, r.data.trust_label, r.data.trust_color);
|
| 1436 |
-
} else {
|
| 1437 |
-
document.getElementById('challengeAnswer').value = '';
|
| 1438 |
-
}
|
| 1439 |
-
}
|
| 1440 |
-
}
|
| 1441 |
-
|
| 1442 |
-
// ── Explainability (public demo) ─────────────────────────
|
| 1443 |
-
async function intelExplain() {
|
| 1444 |
-
var r = await req(INTEL + '/demo/explain', 'POST', {
|
| 1445 |
-
location_score: parseFloat(document.getElementById('expLocation').value),
|
| 1446 |
-
device_score: parseFloat(document.getElementById('expDevice').value),
|
| 1447 |
-
time_score: parseFloat(document.getElementById('expTime').value),
|
| 1448 |
-
velocity_score: parseFloat(document.getElementById('expVelocity').value),
|
| 1449 |
-
behavior_score: parseFloat(document.getElementById('expBehavior').value),
|
| 1450 |
-
security_level: parseInt(document.getElementById('expLevel').value),
|
| 1451 |
-
risk_level: 'medium',
|
| 1452 |
-
}, false);
|
| 1453 |
-
var el = document.getElementById('explainResult');
|
| 1454 |
-
if (!r.ok || !r.data) { el.textContent = JSON.stringify(r.data); return; }
|
| 1455 |
-
var d = r.data;
|
| 1456 |
-
el.innerHTML =
|
| 1457 |
-
'<div style="font-size:12px;color:var(--c-muted);margin-bottom:6px;">' +
|
| 1458 |
-
'\uD83D\uDD0D Audit ID: <code>' + d.audit_id + '</code> | Confidence: ' +
|
| 1459 |
-
(d.confidence*100).toFixed(0) + '% | Action: <em>' + d.action + '</em>' +
|
| 1460 |
-
'</div>' +
|
| 1461 |
-
'<div style="font-size:13px;margin-bottom:10px;padding:8px;background:var(--c-surface);border-radius:6px;">' + d.summary + '</div>' +
|
| 1462 |
-
(d.factors||[]).map(function(f){
|
| 1463 |
-
var col = f.status==='anomalous'?'#ef4444':'#22c55e';
|
| 1464 |
-
var bar = Math.min(100, Math.max(0, Math.abs(f.contribution)*4));
|
| 1465 |
-
return '<div style="margin-bottom:8px;">' +
|
| 1466 |
-
'<div style="display:flex;justify-content:space-between;font-size:12px;">' +
|
| 1467 |
-
'<span>' + f.icon + ' <strong>' + f.factor + '</strong>' +
|
| 1468 |
-
' <span style="color:var(--c-muted);">w:' + f.model_weight + '</span></span>' +
|
| 1469 |
-
'<span style="color:'+col+';">' + (f.contribution>=0?'+':'') + f.contribution.toFixed(1) + '</span>' +
|
| 1470 |
-
'</div>' +
|
| 1471 |
-
'<div style="height:6px;background:#1e293b;border-radius:3px;overflow:hidden;margin-top:3px;">' +
|
| 1472 |
-
'<div style="height:100%;width:'+bar+'%;background:'+col+';border-radius:3px;"></div></div>' +
|
| 1473 |
-
'<div style="font-size:11px;color:var(--c-muted);">' + f.detail + '</div>' +
|
| 1474 |
-
'</div>';
|
| 1475 |
-
}).join('');
|
| 1476 |
-
}
|
| 1477 |
-
|
| 1478 |
-
// ── Session Audit Trail (authed) ─────────────────────────
|
| 1479 |
-
async function intelSessionExplain() {
|
| 1480 |
-
if (!getToken()) { alert('Login first.'); return; }
|
| 1481 |
-
showResp('sessionExplainResp', await req(INTEL + '/explain'));
|
| 1482 |
-
}
|
| 1483 |
-
|
| 1484 |
-
// ── Quick Login ───────────────────────────────────────────
|
| 1485 |
-
async function intelQuickLogin() {
|
| 1486 |
-
var r = await req(AUTH + '/login', 'POST',
|
| 1487 |
-
{ email:'demo.user@adaptive.demo', password:'DemoUser@123!' }, false);
|
| 1488 |
-
var el = document.getElementById('intelLoginStatus');
|
| 1489 |
-
if (r.ok && r.data && r.data.access_token) {
|
| 1490 |
-
saveToken(r.data.access_token);
|
| 1491 |
-
el.textContent = '\u2705 Logged in as demo.user@adaptive.demo';
|
| 1492 |
-
el.style.color = 'var(--c-success)';
|
| 1493 |
-
await intelGetTrust();
|
| 1494 |
-
} else {
|
| 1495 |
-
el.textContent = '\u274C Login failed \u2014 run Setup in Scenario 1 first.';
|
| 1496 |
-
el.style.color = 'var(--c-danger)';
|
| 1497 |
-
}
|
| 1498 |
-
}
|
| 1499 |
-
|
| 1500 |
-
// ── Tab init ──────────────────────────────────────────────
|
| 1501 |
-
async function intelInitTab() {
|
| 1502 |
-
await intelLoadCities();
|
| 1503 |
-
if (getToken()) await intelGetTrust();
|
| 1504 |
-
}
|
| 1505 |
-
|
| 1506 |
-
/* ═════════════════════════════════════════════════════════ */
|
| 1507 |
-
|
| 1508 |
-
/* ---- ADMIN ---- */
|
| 1509 |
-
async function quickAdminLogin() {
|
| 1510 |
-
showResp('adminLoginResp', await req(AUTH + '/login', 'POST', { email: 'demo.admin@adaptive.demo', password: 'Admin@Demo456!' }, false), true);
|
| 1511 |
-
}
|
| 1512 |
-
|
| 1513 |
-
async function checkEmailStatus() {
|
| 1514 |
-
var body = document.getElementById('emailStatusBody');
|
| 1515 |
-
body.textContent = 'Checking…';
|
| 1516 |
-
var r = await req(ADMIN + '/email-status');
|
| 1517 |
-
if (!r.ok || !r.data) {
|
| 1518 |
-
body.innerHTML = '<span style="color:var(--c-danger);">Failed to fetch — are you logged in as admin?</span>';
|
| 1519 |
-
return;
|
| 1520 |
-
}
|
| 1521 |
-
var d = r.data;
|
| 1522 |
-
var configured = d.configured;
|
| 1523 |
-
var statusPill = configured
|
| 1524 |
-
? '<span class="pill pill-ok">✔ Configured</span>'
|
| 1525 |
-
: '<span class="pill pill-err">✘ Not Configured</span>';
|
| 1526 |
-
var fields = d.fields || {};
|
| 1527 |
-
var rows = Object.entries(fields).map(function(kv) {
|
| 1528 |
-
var k = kv[0], v = kv[1];
|
| 1529 |
-
var pill = v ? '<span class="pill pill-ok">Set</span>' : '<span class="pill pill-err">Missing</span>';
|
| 1530 |
-
return '<div style="display:flex;justify-content:space-between;align-items:center;padding:5px 0;border-bottom:1px solid var(--c-border);font-size:12px;"><span style="font-family:\'Consolas\',monospace;color:var(--c-text);">ADAPTIVEAUTH_' + k + '</span>' + pill + '</div>';
|
| 1531 |
-
}).join('');
|
| 1532 |
-
var instructions = configured ? '' : '<div class="callout callout-warn" style="margin-top:12px;font-size:12px;"><strong>Setup:</strong> Create a <code>.env</code> file in the project root with the missing fields above.<br><br><code>' + (d.setup_instructions||'') + '</code></div>';
|
| 1533 |
-
body.innerHTML = '<div style="margin-bottom:12px;">' + statusPill +
|
| 1534 |
-
(d.mail_port ? ' <span style="font-size:12px;color:var(--c-muted);">Port: ' + d.mail_port + ' STARTTLS: ' + (d.starttls ? 'Yes' : 'No') + '</span>' : '') +
|
| 1535 |
-
'</div>' + rows + instructions;
|
| 1536 |
-
}
|
| 1537 |
-
|
| 1538 |
-
async function loadAdminStats() {
|
| 1539 |
-
const r = await req(ADMIN + '/statistics');
|
| 1540 |
-
if (!r.ok || !r.data) return;
|
| 1541 |
-
var d = r.data;
|
| 1542 |
-
document.getElementById('statsGrid').innerHTML =
|
| 1543 |
-
'<div class="stat-box"><div class="stat-num" style="color:var(--c-info);">' + (d.total_users !== undefined ? d.total_users : '—') + '</div><div class="stat-label">Total Users</div></div>' +
|
| 1544 |
-
'<div class="stat-box"><div class="stat-num" style="color:var(--c-success);">' + (d.active_sessions !== undefined ? d.active_sessions : '—') + '</div><div class="stat-label">Active Sessions</div></div>' +
|
| 1545 |
-
'<div class="stat-box"><div class="stat-num" style="color:var(--c-danger);">' + (d.high_risk_events_today !== undefined ? d.high_risk_events_today : '—') + '</div><div class="stat-label">High Risk Today</div></div>' +
|
| 1546 |
-
'<div class="stat-box"><div class="stat-num" style="color:var(--c-warn);">' + (d.failed_logins_today !== undefined ? d.failed_logins_today : '—') + '</div><div class="stat-label">Failed Logins</div></div>';
|
| 1547 |
-
}
|
| 1548 |
-
|
| 1549 |
-
async function adminGetUsers() { showResp('adminUserResp', await req(ADMIN + '/users')); }
|
| 1550 |
-
async function adminGetSessions() { showResp('adminUserResp', await req(ADMIN + '/sessions')); }
|
| 1551 |
-
async function adminBlock() { var uid = document.getElementById('adminUserId').value; if (!uid) { alert('Enter user ID.'); return; } showResp('adminUserResp', await req(ADMIN + '/users/' + uid + '/block', 'POST')); }
|
| 1552 |
-
async function adminUnblock() { var uid = document.getElementById('adminUserId').value; if (!uid) { alert('Enter user ID.'); return; } showResp('adminUserResp', await req(ADMIN + '/users/' + uid + '/unblock', 'POST')); }
|
| 1553 |
-
async function adminGetRiskEvents() { showResp('adminRiskResp', await req(ADMIN + '/risk-events')); }
|
| 1554 |
-
async function adminGetAnomalies() { showResp('adminRiskResp', await req(ADMIN + '/anomalies')); }
|
| 1555 |
-
async function adminRiskOverview() { showResp('adminRiskResp', await req(RISK + '/overview')); }
|
| 1556 |
-
async function adminRiskStatsPeriod(p) { showResp('statsResp', await req(ADMIN + '/risk-statistics?period=' + p)); }
|
| 1557 |
-
|
| 1558 |
-
window.addEventListener('load', function() {
|
| 1559 |
-
refreshTokenBar();
|
| 1560 |
-
ping();
|
| 1561 |
-
// Background passive refresh (only when monitoring is NOT active)
|
| 1562 |
-
setInterval(function() {
|
| 1563 |
-
if (document.getElementById('tab-demo2').classList.contains('active') && !monitorInterval) {
|
| 1564 |
-
refreshAnomalies();
|
| 1565 |
-
}
|
| 1566 |
-
}, 10000);
|
| 1567 |
-
});
|
| 1568 |
-
</script>
|
| 1569 |
-
</body>
|
| 1570 |
-
</html>
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>AdaptiveAuth — Framework Demo</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 9 |
+
<link
|
| 10 |
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
|
| 11 |
+
rel="stylesheet"
|
| 12 |
+
/>
|
| 13 |
+
<script type="module" crossorigin src="/static/assets/index-C4kGMRC-.js"></script>
|
| 14 |
+
<link rel="stylesheet" crossorigin href="/static/assets/index-DKV7YaMs.css">
|
| 15 |
+
</head>
|
| 16 |
+
<body>
|
| 17 |
+
<div id="root"></div>
|
| 18 |
+
</body>
|
| 19 |
+
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|