Add base project

This commit is contained in:
Azmat-7860
2026-10-17 19:13:31 +05:30
commit dbb5ce3fd2
46 changed files with 5460 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# AuthSec Backend Configuration
# Points to Spring Boot backend service
VITE_API_URL=http://localhost:9292
+2
View File
@@ -0,0 +1,2 @@
# AuthSec Backend Configuration
VITE_API_URL=http://localhost:9292
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "oxc"],
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
+16
View File
@@ -0,0 +1,16 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the Oxlint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and Oxlint's TypeScript related rules in your project.
+18
View File
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AuthSec - Identity & Access Security Platform</title>
<meta name="description" content="AuthSec modern authentication and security dashboard base platform" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+1336
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "authsec_react",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "oxlint",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^1.52.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-router-dom": "^7.18.4"
},
"devDependencies": {
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.7",
"@vitejs/plugin-react": "^6.1.1",
"oxlint": "^1.81.0",
"vite": "^8.3.0"
}
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

+1
View File
@@ -0,0 +1 @@
/* App styles managed by global and module css */
+71
View File
@@ -0,0 +1,71 @@
import React from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { ThemeProvider } from './context/ThemeContext';
import { AuthProvider, useAuth } from './context/AuthContext';
import { Login } from './pages/Login';
import { Register } from './pages/Register';
import { Dashboard } from './pages/Dashboard';
import { NotFound } from './pages/NotFound';
import { ProtectedRoute } from './components/auth/ProtectedRoute';
import './styles/global.css';
// Root redirector based on authentication state
function HomeRedirect() {
const { isAuthenticated, loading } = useAuth();
if (loading) return null;
return isAuthenticated ? <Navigate to="/dashboard" replace /> : <Navigate to="/login" replace />;
}
// Redirect away from login/register if already logged in
function PublicOnlyRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
if (loading) return null;
return isAuthenticated ? <Navigate to="/dashboard" replace /> : children;
}
export function App() {
return (
<ThemeProvider>
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<HomeRedirect />} />
<Route
path="/login"
element={
<PublicOnlyRoute>
<Login />
</PublicOnlyRoute>
}
/>
<Route
path="/register"
element={
<PublicOnlyRoute>
<Register />
</PublicOnlyRoute>
}
/>
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
{/* === BUILDER:ROUTES (do not remove) === */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</AuthProvider>
</ThemeProvider>
);
}
export default App;
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

+35
View File
@@ -0,0 +1,35 @@
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
export function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
const location = useLocation();
if (loading) {
return (
<div
style={{
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '16px',
background: 'var(--bg-app)',
}}
>
<div className="btn-spinner" style={{ width: 36, height: 36, borderWidth: 3 }} />
<span style={{ fontSize: '0.9rem', color: 'var(--text-muted)' }}>
Validating Security Session...
</span>
</div>
);
}
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
import './common.css';
export function Badge({ children, variant = 'primary', size = 'sm', className = '' }) {
return (
<span className={`badge badge-${variant} badge-${size} ${className}`}>
{children}
</span>
);
}
export function Card({ children, className = '', hoverable = false, ...props }) {
return (
<div className={`custom-card ${hoverable ? 'card-hoverable' : ''} ${className}`} {...props}>
{children}
</div>
);
}
+31
View File
@@ -0,0 +1,31 @@
import React from 'react';
import './common.css';
export function Button({
children,
variant = 'primary',
size = 'md',
isLoading = false,
icon: Icon,
fullWidth = false,
className = '',
disabled,
...props
}) {
return (
<button
className={`btn btn-${variant} btn-${size} ${fullWidth ? 'btn-block' : ''} ${className}`}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? (
<span className="btn-spinner" />
) : (
<>
{Icon && <Icon className="btn-icon" size={size === 'sm' ? 16 : size === 'lg' ? 20 : 18} />}
<span>{children}</span>
</>
)}
</button>
);
}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import './common.css';
export function Card({ children, className = '', hoverable = false, ...props }) {
return (
<div className={`custom-card ${hoverable ? 'card-hoverable' : ''} ${className}`} {...props}>
{children}
</div>
);
}
+65
View File
@@ -0,0 +1,65 @@
import React, { useState } from 'react';
import { Eye, EyeOff } from 'lucide-react';
import './common.css';
export function Input({
label,
type = 'text',
icon: Icon,
error,
helperText,
id,
className = '',
required = false,
...props
}) {
const [showPassword, setShowPassword] = useState(false);
const isPasswordType = type === 'password';
const effectiveType = isPasswordType ? (showPassword ? 'text' : 'password') : type;
const inputId = id || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined);
return (
<div className={`input-group ${error ? 'has-error' : ''} ${className}`}>
{label && (
<label htmlFor={inputId} className="input-label">
{label}
{required && <span className="input-required">*</span>}
</label>
)}
<div className="input-wrapper">
{Icon && (
<span className="input-prefix-icon">
<Icon size={18} />
</span>
)}
<input
id={inputId}
type={effectiveType}
className={`input-field ${Icon ? 'with-prefix' : ''} ${isPasswordType ? 'with-suffix' : ''}`}
{...props}
/>
{isPasswordType && (
<button
type="button"
className="input-suffix-btn"
onClick={() => setShowPassword(!showPassword)}
tabIndex={-1}
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
)}
</div>
{error ? (
<p className="input-error-text">{error}</p>
) : helperText ? (
<p className="input-helper-text">{helperText}</p>
) : null}
</div>
);
}
+30
View File
@@ -0,0 +1,30 @@
import React from 'react';
import { AlertCircle, CheckCircle2, Info, AlertTriangle, X } from 'lucide-react';
import './common.css';
export function Toast({ type = 'info', message, onClose, className = '' }) {
if (!message) return null;
const icons = {
error: AlertCircle,
success: CheckCircle2,
warning: AlertTriangle,
info: Info,
};
const IconComponent = icons[type] || Info;
return (
<div className={`toast-notification toast-${type} animate-fade-in ${className}`}>
<div className="toast-icon">
<IconComponent size={20} />
</div>
<div className="toast-message">{message}</div>
{onClose && (
<button className="toast-close" onClick={onClose} aria-label="Close notification">
<X size={16} />
</button>
)}
</div>
);
}
+344
View File
@@ -0,0 +1,344 @@
/* Common Components CSS */
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 600;
border-radius: var(--radius-md);
transition: all var(--transition-normal);
user-select: none;
white-space: nowrap;
gap: 8px;
position: relative;
overflow: hidden;
}
.btn:active:not(:disabled) {
transform: scale(0.98);
}
.btn-block {
width: 100%;
}
.btn-sm {
padding: 8px 14px;
font-size: 0.825rem;
}
.btn-md {
padding: 11px 20px;
font-size: 0.925rem;
}
.btn-lg {
padding: 14px 28px;
font-size: 1.05rem;
}
.btn-primary {
background: var(--gradient-primary);
color: #ffffff;
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35);
border: 1px solid rgba(255, 255, 255, 0.15);
}
.btn-primary:hover:not(:disabled) {
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.5);
filter: brightness(1.08);
}
.btn-secondary {
background: var(--bg-surface-elevated);
color: var(--text-main);
border: 1px solid var(--border-subtle);
}
.btn-secondary:hover:not(:disabled) {
background: var(--bg-surface);
border-color: var(--border-glow);
}
.btn-outline {
background: transparent;
color: var(--text-main);
border: 1px solid var(--border-subtle);
}
.btn-outline:hover:not(:disabled) {
border-color: var(--color-primary);
color: var(--color-primary-light);
background: rgba(99, 102, 241, 0.08);
}
.btn-ghost {
background: transparent;
color: var(--text-secondary);
}
.btn-ghost:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.06);
color: var(--text-main);
}
.btn:disabled {
opacity: 0.55;
cursor: not-allowed;
filter: grayscale(0.4);
}
.btn-spinner {
width: 18px;
height: 18px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: #ffffff;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
/* Inputs */
.input-group {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
}
.input-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 4px;
}
.input-required {
color: var(--color-danger);
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
width: 100%;
}
.input-field {
width: 100%;
padding: 12px 16px;
font-size: 0.95rem;
background: var(--bg-input);
color: var(--text-main);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
outline: none;
transition: all var(--transition-normal);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
}
.input-field::placeholder {
color: var(--text-muted);
}
.input-field:focus {
border-color: var(--border-focus);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
}
.input-field.with-prefix {
padding-left: 44px;
}
.input-field.with-suffix {
padding-right: 44px;
}
.input-prefix-icon {
position: absolute;
left: 14px;
color: var(--text-muted);
display: flex;
align-items: center;
pointer-events: none;
transition: color var(--transition-fast);
}
.input-wrapper:focus-within .input-prefix-icon {
color: var(--color-primary-light);
}
.input-suffix-btn {
position: absolute;
right: 12px;
color: var(--text-muted);
display: flex;
align-items: center;
padding: 4px;
border-radius: var(--radius-sm);
transition: color var(--transition-fast);
}
.input-suffix-btn:hover {
color: var(--text-main);
}
.input-group.has-error .input-field {
border-color: var(--color-danger);
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15);
}
.input-error-text {
font-size: 0.8rem;
color: var(--color-danger);
margin-top: 2px;
}
.input-helper-text {
font-size: 0.8rem;
color: var(--text-muted);
}
/* Badges */
.badge {
display: inline-flex;
align-items: center;
gap: 5px;
font-weight: 600;
border-radius: var(--radius-full);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.badge-sm {
padding: 3px 8px;
font-size: 0.7rem;
}
.badge-md {
padding: 5px 12px;
font-size: 0.775rem;
}
.badge-primary {
background: rgba(99, 102, 241, 0.15);
color: var(--color-primary-light);
border: 1px solid rgba(99, 102, 241, 0.3);
}
.badge-success {
background: var(--color-success-bg);
color: var(--color-success);
border: 1px solid rgba(16, 185, 129, 0.3);
}
.badge-warning {
background: var(--color-warning-bg);
color: var(--color-warning);
border: 1px solid rgba(245, 158, 11, 0.3);
}
.badge-danger {
background: var(--color-danger-bg);
color: var(--color-danger);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.badge-info {
background: var(--color-info-bg);
color: var(--color-info);
border: 1px solid rgba(59, 130, 246, 0.3);
}
/* Custom Card */
.custom-card {
background: var(--bg-glass-card);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
padding: 24px;
transition: all var(--transition-normal);
}
.custom-card.card-hoverable:hover {
transform: translateY(-3px);
border-color: rgba(99, 102, 241, 0.3);
box-shadow: var(--shadow-lg), var(--shadow-glow);
}
/* Toast Notification */
.toast-notification {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px 18px;
border-radius: var(--radius-md);
margin-bottom: 18px;
width: 100%;
}
.toast-icon {
flex-shrink: 0;
display: flex;
align-items: center;
}
.toast-message {
flex: 1;
font-size: 0.9rem;
line-height: 1.4;
word-break: break-word;
}
.toast-close {
flex-shrink: 0;
opacity: 0.7;
transition: opacity var(--transition-fast);
}
.toast-close:hover {
opacity: 1;
}
.toast-error {
background: var(--color-danger-bg);
color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.3);
}
[data-theme='light'] .toast-error {
color: #b91c1c;
}
.toast-success {
background: var(--color-success-bg);
color: #6ee7b7;
border: 1px solid rgba(16, 185, 129, 0.3);
}
[data-theme='light'] .toast-success {
color: #047857;
}
.toast-warning {
background: var(--color-warning-bg);
color: #fcd34d;
border: 1px solid rgba(245, 158, 11, 0.3);
}
[data-theme='light'] .toast-warning {
color: #b45309;
}
.toast-info {
background: var(--color-info-bg);
color: #93c5fd;
border: 1px solid rgba(59, 130, 246, 0.3);
}
[data-theme='light'] .toast-info {
color: #1d4ed8;
}
@@ -0,0 +1,58 @@
.product-page {
display: grid;
gap: 16px;
padding: 8px;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 12px;
margin: 12px 0;
}
.product-field {
display: flex;
flex-direction: column;
gap: 4px;
}
.product-wide {
grid-column: 1 / -1;
}
.product-check {
display: flex;
align-items: center;
gap: 8px;
}
.product-actions {
display: flex;
gap: 10px;
margin-top: 8px;
}
.product-error {
color: #b91c1c;
}
.product-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 8px;
}
.product-list li {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.product-row-actions {
display: flex;
gap: 8px;
}
+178
View File
@@ -0,0 +1,178 @@
import React, { useEffect, useState } from 'react';
import { getProducts, createProduct, updateProduct, deleteProduct } from './ProductService';
import { Card } from '../../common/Card';
import { Input } from '../../common/Input';
import { Button } from '../../common/Button';
import './Product.css';
const EMPTY_FORM = {
name: '',
description: '',
email: '',
price: '',
quantity: '',
dob: '',
createdAt: '',
isActive: true,
status: '',
tags: [],
photo: null,
categoryId: '',
};
const STATUS_OPTIONS = ['New', 'In Stock', 'Discontinued'];
const TAG_OPTIONS = ['Electronics', 'Food', 'Clothing', 'Books'];
const CATEGORY_OPTIONS = [
{ id: 'cat-1', label: 'Category 1' },
{ id: 'cat-2', label: 'Category 2' },
];
export function Product() {
const [items, setItems] = useState([]);
const [formData, setFormData] = useState(EMPTY_FORM);
const [selectedId, setSelectedId] = useState(null);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const fetchData = async () => {
try {
const d = await getProducts();
const list = Array.isArray(d) ? d : d?.data ?? d?.items ?? [];
setItems(list);
} catch (e) {
setError(e.message);
}
};
useEffect(() => {
fetchData();
}, []);
const handleChange = (e) => {
const { name, type, checked, files, value } = e.target;
if (type === 'file') {
setFormData((p) => ({ ...p, [name]: files && files[0] ? files[0].name : null }));
return;
}
setFormData((p) => ({ ...p, [name]: type === 'checkbox' ? checked : value }));
};
const handleMultiChange = (e) => {
const { name, options } = e.target;
const vals = Array.from(options)
.filter((o) => o.selected)
.map((o) => o.value);
setFormData((p) => ({ ...p, [name]: vals }));
};
const handleSubmit = async (e) => {
e.preventDefault();
setSaving(true);
setError('');
try {
if (selectedId) {
await updateProduct(selectedId, formData);
} else {
await createProduct(formData);
}
setFormData(EMPTY_FORM);
setSelectedId(null);
await fetchData();
} catch (err) {
setError(err.message);
} finally {
setSaving(false);
}
};
const handleEdit = (item) => {
setSelectedId(item.id);
setFormData({ ...EMPTY_FORM, ...item });
};
const handleDelete = async (id) => {
await deleteProduct(id);
await fetchData();
};
const reset = () => {
setFormData(EMPTY_FORM);
setSelectedId(null);
setError('');
};
return (
<div className="product-page">
<Card>
<h2>{selectedId ? 'Update Product' : 'Add Product'}</h2>
{error && <p className="product-error">{error}</p>}
<form onSubmit={handleSubmit}>
<div className="product-grid">
<Input label="Name" name="name" value={formData.name || ''} onChange={handleChange} required />
<Input label="Email" type="email" name="email" value={formData.email || ''} onChange={handleChange} />
<Input label="Price" type="number" step="0.01" name="price" value={formData.price || ''} onChange={handleChange} />
<Input label="Quantity" type="number" name="quantity" value={formData.quantity || ''} onChange={handleChange} />
<Input label="Date of Birth" type="date" name="dob" value={formData.dob || ''} onChange={handleChange} />
<Input label="Created At" type="datetime-local" name="createdAt" value={formData.createdAt || ''} onChange={handleChange} />
<label className="product-check">
<input type="checkbox" name="isActive" checked={!!formData.isActive} onChange={handleChange} /> Active
</label>
<label className="product-field">
Status
<select name="status" value={formData.status || ''} onChange={handleChange}>
<option value="">Select status</option>
{STATUS_OPTIONS.map((s) => (
<option key={s} value={s}>{s}</option>
))}
</select>
</label>
<label className="product-field">
Tags
<select multiple name="tags" value={formData.tags || []} onChange={handleMultiChange}>
{TAG_OPTIONS.map((t) => (
<option key={t} value={t}>{t}</option>
))}
</select>
</label>
<label className="product-field">
Photo
<input type="file" name="photo" onChange={handleChange} />
</label>
<label className="product-field">
Category
<select name="categoryId" value={formData.categoryId || ''} onChange={handleChange}>
<option value="">Select category</option>
{CATEGORY_OPTIONS.map((c) => (
<option key={c.id} value={c.id}>{c.label}</option>
))}
</select>
</label>
<label className="product-field product-wide">
Description
<textarea name="description" value={formData.description || ''} onChange={handleChange} rows={3} />
</label>
</div>
<div className="product-actions">
<Button type="submit" isLoading={saving}>{selectedId ? 'Update' : 'Add'}</Button>
<Button type="button" variant="outline" onClick={reset}>Reset</Button>
</div>
</form>
</Card>
<Card>
<h2>Products</h2>
<ul className="product-list">
{items.map((item) => (
<li key={item.id}>
<span>{item.name} — {item.email} — {item.price} — {item.status}</span>
<span className="product-row-actions">
<Button size="sm" variant="secondary" onClick={() => handleEdit(item)}>Edit</Button>
<Button size="sm" variant="outline" onClick={() => handleDelete(item.id)}>Delete</Button>
</span>
</li>
))}
</ul>
</Card>
</div>
);
}
@@ -0,0 +1,16 @@
import { request } from '../../../../services/api';
const ENDPOINT = '/Product/Product';
export const getProducts = () => request(ENDPOINT);
export const getProductById = (id) => request(`${ENDPOINT}/${id}`);
export const createProduct = (data) =>
request(ENDPOINT, { method: 'POST', body: JSON.stringify(data) });
export const updateProduct = (id, data) =>
request(`${ENDPOINT}/${id}`, { method: 'PUT', body: JSON.stringify(data) });
export const deleteProduct = (id) =>
request(`${ENDPOINT}/${id}`, { method: 'DELETE' });
+26
View File
@@ -0,0 +1,26 @@
import React, { useState } from 'react';
import { Sidebar } from './Sidebar';
import { Header } from './Header';
import './layout.css';
export function DashboardLayout({ children, activeTab, setActiveTab }) {
const [collapsed, setCollapsed] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
return (
<div className="dashboard-layout">
<Sidebar
collapsed={collapsed}
setCollapsed={setCollapsed}
mobileOpen={mobileOpen}
setMobileOpen={setMobileOpen}
activeTab={activeTab}
setActiveTab={setActiveTab}
/>
<div className="dashboard-main">
<Header setMobileOpen={setMobileOpen} />
<main className="dashboard-content-area">{children}</main>
</div>
</div>
);
}
+129
View File
@@ -0,0 +1,129 @@
import React, { useState, useRef, useEffect } from 'react';
import { Menu, Search, Bell, User as UserIcon, LogOut, ShieldCheck, ChevronDown } from 'lucide-react';
import { useAuth } from '../../context/AuthContext';
import { ThemeToggle } from './ThemeToggle';
import './layout.css';
export function Header({ setMobileOpen }) {
const { user, logout, isDemoMode } = useAuth();
const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = useRef(null);
// Close dropdown on click outside
useEffect(() => {
function handleClickOutside(event) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setDropdownOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<header className="dashboard-header">
{/* Left: Mobile hamburger & Search */}
<div className="header-left">
<button
className="mobile-menu-btn"
onClick={() => setMobileOpen(true)}
aria-label="Open navigation menu"
>
<Menu size={22} />
</button>
<div className="header-search">
<Search size={18} className="header-search-icon" />
<input
type="text"
className="header-search-input"
placeholder="Search workspaces, logs, users..."
/>
</div>
</div>
{/* Right: Actions, Theme, User */}
<div className="header-right">
{/* Backend Connectivity Status Badge */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
fontSize: '0.8rem',
padding: '4px 10px',
borderRadius: 'var(--radius-full)',
background: isDemoMode ? 'var(--color-warning-bg)' : 'var(--color-success-bg)',
color: isDemoMode ? 'var(--color-warning)' : 'var(--color-success)',
border: `1px solid ${isDemoMode ? 'rgba(245, 158, 11, 0.3)' : 'rgba(16, 185, 129, 0.3)'}`,
}}
title={isDemoMode ? 'Running in Standalone Demo Mode' : 'Connected to Spring Boot API'}
>
<span
style={{
width: 7,
height: 7,
borderRadius: '50%',
backgroundColor: isDemoMode ? 'var(--color-warning)' : 'var(--color-success)',
}}
/>
<span>{isDemoMode ? 'Demo Sandbox' : 'Backend Live'}</span>
</div>
{/* Theme Switcher */}
<ThemeToggle />
{/* Notifications */}
<button className="header-btn" title="Recent security alerts">
<Bell size={18} />
<span className="header-badge-dot" />
</button>
{/* User Dropdown */}
<div className="profile-dropdown-wrapper" ref={dropdownRef}>
<button
className="profile-trigger-btn"
onClick={() => setDropdownOpen(!dropdownOpen)}
aria-expanded={dropdownOpen}
>
<div className="sidebar-user-avatar" style={{ width: 30, height: 30, fontSize: '0.75rem' }}>
{(user?.firstName?.[0] || 'A')}
</div>
<span style={{ fontSize: '0.85rem', fontWeight: 600 }}>
{user?.firstName || 'User'}
</span>
<ChevronDown size={14} color="var(--text-muted)" />
</button>
{dropdownOpen && (
<div className="profile-dropdown-menu animate-fade-in">
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border-subtle)' }}>
<p style={{ fontSize: '0.825rem', fontWeight: 700 }}>{user?.fullname || user?.firstName}</p>
<p style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>{user?.email}</p>
</div>
<div className="dropdown-item" onClick={() => setDropdownOpen(false)}>
<UserIcon size={16} />
<span>Profile Settings</span>
</div>
<div className="dropdown-item" onClick={() => setDropdownOpen(false)}>
<ShieldCheck size={16} />
<span>Security Token Info</span>
</div>
<div
className="dropdown-item danger"
onClick={() => {
setDropdownOpen(false);
logout();
}}
>
<LogOut size={16} />
<span>Sign Out</span>
</div>
</div>
)}
</div>
</div>
</header>
);
}
+134
View File
@@ -0,0 +1,134 @@
import React from 'react';
import {
Shield,
LayoutDashboard,
Layers,
ShieldAlert,
Sliders,
Users,
Settings,
FileText,
ChevronLeft,
ChevronRight,
LogOut,
} from 'lucide-react';
import { useAuth } from '../../context/AuthContext';
import { useNavigate } from 'react-router-dom';
import './layout.css';
export function Sidebar({ collapsed, setCollapsed, mobileOpen, setMobileOpen, activeTab, setActiveTab }) {
const { user, logout } = useAuth();
const navigate = useNavigate();
const navItems = [
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
{ id: 'workspaces', label: 'Workspaces', icon: Layers },
{ id: 'security', label: 'Security Audit', icon: ShieldAlert },
{ id: 'system', label: 'System Parameters', icon: Sliders },
{ id: 'users', label: 'User Directory', icon: Users },
{ id: 'settings', label: 'Settings', icon: Settings },
// === BUILDER:NAV-ITEMS (do not remove) ===
];
const handleItemClick = (id) => {
const target = navItems.find((n) => n.id === id);
if (target && target.path) {
navigate(target.path);
if (mobileOpen) {
setMobileOpen(false);
}
return;
}
setActiveTab(id);
if (mobileOpen) {
setMobileOpen(false);
}
};
const getInitials = (name) => {
if (!name) return 'AU';
return name
.split(' ')
.map((n) => n[0])
.join('')
.slice(0, 2)
.toUpperCase();
};
return (
<>
{/* Mobile Backdrop */}
{mobileOpen && (
<div className="sidebar-overlay" onClick={() => setMobileOpen(false)} />
)}
<aside className={`dashboard-sidebar ${collapsed ? 'collapsed' : ''} ${mobileOpen ? 'mobile-open' : ''}`}>
{/* Brand Header */}
<div className="sidebar-header">
<div className="sidebar-brand">
<div className="sidebar-brand-icon">
<Shield size={22} />
</div>
{!collapsed && <span className="sidebar-brand-title">AuthSec</span>}
</div>
<button
className="sidebar-collapse-btn"
onClick={() => setCollapsed(!collapsed)}
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
>
{collapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
</button>
</div>
{/* Navigation Items */}
<div className="sidebar-nav">
{!collapsed && <span className="sidebar-section-title">Navigation</span>}
{navItems.map((item) => {
const Icon = item.icon;
const isActive = activeTab === item.id;
return (
<div
key={item.id}
className={`sidebar-item ${isActive ? 'active' : ''}`}
onClick={() => handleItemClick(item.id)}
title={collapsed ? item.label : undefined}
>
<Icon size={20} className="sidebar-item-icon" />
{!collapsed && <span>{item.label}</span>}
</div>
);
})}
</div>
{/* User Card in Footer */}
<div className="sidebar-footer">
<div
className="sidebar-user-pill"
onClick={logout}
title={collapsed ? `${user?.firstName || 'User'} (Click to Logout)` : undefined}
>
<div className="sidebar-user-avatar">
{getInitials(user?.fullname || user?.firstName || user?.email)}
</div>
{!collapsed && (
<>
<div className="sidebar-user-details">
<span className="sidebar-user-name">
{user?.fullname || user?.firstName || 'Secured User'}
</span>
<span className="sidebar-user-role">
{user?.roles?.[0] || 'ROLE_ADMIN'}
</span>
</div>
<LogOut size={16} style={{ marginLeft: 'auto', color: 'var(--text-muted)' }} />
</>
)}
</div>
</div>
</aside>
</>
);
}
+21
View File
@@ -0,0 +1,21 @@
import React from 'react';
import { Sun, Moon } from 'lucide-react';
import { useTheme } from '../../context/ThemeContext';
import './layout.css';
export function ThemeToggle({ className = '' }) {
const { theme, toggleTheme, isDark } = useTheme();
return (
<button
className={`theme-toggle-btn ${className}`}
onClick={toggleTheme}
title={`Switch to ${isDark ? 'light' : 'dark'} mode`}
aria-label="Toggle visual theme"
>
<div className={`theme-toggle-inner ${isDark ? 'is-dark' : 'is-light'}`}>
{isDark ? <Moon size={18} className="theme-icon" /> : <Sun size={18} className="theme-icon" />}
</div>
</button>
);
}
+424
View File
@@ -0,0 +1,424 @@
/* Layout CSS (Header, Sidebar, Navigation) */
/* Theme Toggle Button */
.theme-toggle-btn {
width: 38px;
height: 38px;
border-radius: var(--radius-md);
background: var(--bg-surface-elevated);
border: 1px solid var(--border-subtle);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: all var(--transition-normal);
}
.theme-toggle-btn:hover {
background: var(--bg-surface);
color: var(--color-primary-light);
border-color: var(--border-glow);
}
.theme-toggle-inner {
display: flex;
align-items: center;
justify-content: center;
transition: transform var(--transition-smooth);
}
/* Dashboard Layout Structure */
.dashboard-layout {
display: flex;
min-height: 100vh;
background-color: var(--bg-app);
}
/* Sidebar */
.dashboard-sidebar {
width: 260px;
background: var(--bg-surface);
border-right: 1px solid var(--border-subtle);
display: flex;
flex-direction: column;
position: sticky;
top: 0;
height: 100vh;
z-index: 50;
transition: width var(--transition-smooth), transform var(--transition-smooth);
}
.dashboard-sidebar.collapsed {
width: 78px;
}
.sidebar-header {
height: 72px;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-subtle);
}
.sidebar-brand {
display: flex;
align-items: center;
gap: 12px;
overflow: hidden;
}
.sidebar-brand-icon {
width: 38px;
height: 38px;
border-radius: var(--radius-md);
background: var(--gradient-primary);
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
flex-shrink: 0;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
}
.sidebar-brand-title {
font-size: 1.25rem;
font-weight: 800;
color: var(--text-main);
white-space: nowrap;
}
.sidebar-collapse-btn {
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
transition: all var(--transition-fast);
}
.sidebar-collapse-btn:hover {
background: var(--bg-surface-elevated);
color: var(--text-main);
}
/* Sidebar Nav */
.sidebar-nav {
flex: 1;
padding: 20px 14px;
display: flex;
flex-direction: column;
gap: 6px;
overflow-y: auto;
}
.sidebar-section-title {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
padding: 8px 10px;
margin-top: 8px;
}
.sidebar-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
border-radius: var(--radius-md);
color: var(--text-secondary);
font-weight: 500;
font-size: 0.925rem;
transition: all var(--transition-fast);
cursor: pointer;
white-space: nowrap;
position: relative;
}
.sidebar-item:hover {
background: var(--bg-surface-elevated);
color: var(--text-main);
}
.sidebar-item.active {
background: rgba(99, 102, 241, 0.12);
color: var(--color-primary-light);
font-weight: 600;
}
.sidebar-item.active::before {
content: '';
position: absolute;
left: 0;
top: 15%;
bottom: 15%;
width: 3px;
background: var(--color-primary);
border-radius: 0 var(--radius-xs) var(--radius-xs) 0;
}
.sidebar-item-icon {
flex-shrink: 0;
}
/* Sidebar Footer User Card */
.sidebar-footer {
padding: 16px;
border-top: 1px solid var(--border-subtle);
}
.sidebar-user-pill {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 10px;
border-radius: var(--radius-md);
background: var(--bg-surface-elevated);
cursor: pointer;
transition: all var(--transition-fast);
overflow: hidden;
}
.sidebar-user-pill:hover {
background: rgba(99, 102, 241, 0.08);
}
.sidebar-user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--gradient-primary);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: #ffffff;
flex-shrink: 0;
}
.sidebar-user-details {
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-user-name {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-main);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-user-role {
font-size: 0.725rem;
color: var(--color-primary-light);
white-space: nowrap;
}
/* Main Content Area */
.dashboard-main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
/* Header */
.dashboard-header {
height: 72px;
padding: 0 32px;
display: flex;
align-items: center;
justify-content: space-between;
background: var(--bg-glass);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid var(--border-subtle);
position: sticky;
top: 0;
z-index: 40;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.mobile-menu-btn {
display: none;
width: 36px;
height: 36px;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
color: var(--text-secondary);
}
.header-search {
position: relative;
display: flex;
align-items: center;
width: 300px;
}
.header-search-input {
width: 100%;
padding: 8px 14px 8px 38px;
background: var(--bg-surface-elevated);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-full);
color: var(--text-main);
font-size: 0.875rem;
outline: none;
transition: all var(--transition-fast);
}
.header-search-input:focus {
border-color: var(--border-focus);
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
}
.header-search-icon {
position: absolute;
left: 12px;
color: var(--text-muted);
pointer-events: none;
}
.header-right {
display: flex;
align-items: center;
gap: 14px;
}
.header-btn {
width: 38px;
height: 38px;
border-radius: var(--radius-md);
background: var(--bg-surface-elevated);
border: 1px solid var(--border-subtle);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: all var(--transition-fast);
position: relative;
}
.header-btn:hover {
color: var(--text-main);
border-color: var(--border-glow);
}
.header-badge-dot {
position: absolute;
top: 8px;
right: 8px;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--color-secondary);
}
/* Profile Menu Dropdown */
.profile-dropdown-wrapper {
position: relative;
}
.profile-trigger-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 10px 4px 4px;
border-radius: var(--radius-full);
background: var(--bg-surface-elevated);
border: 1px solid var(--border-subtle);
cursor: pointer;
transition: all var(--transition-fast);
}
.profile-trigger-btn:hover {
border-color: var(--border-glow);
}
.profile-dropdown-menu {
position: absolute;
right: 0;
top: calc(100% + 8px);
width: 220px;
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
padding: 8px;
z-index: 100;
display: flex;
flex-direction: column;
gap: 4px;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
border-radius: var(--radius-sm);
color: var(--text-secondary);
font-size: 0.875rem;
cursor: pointer;
transition: all var(--transition-fast);
}
.dropdown-item:hover {
background: var(--bg-surface-elevated);
color: var(--text-main);
}
.dropdown-item.danger:hover {
background: var(--color-danger-bg);
color: var(--color-danger);
}
/* Responsive Mobile Sidebar overlay */
.sidebar-overlay {
display: none;
}
@media (max-width: 900px) {
.dashboard-header {
padding: 0 16px;
}
.header-search {
display: none;
}
.mobile-menu-btn {
display: flex;
}
.dashboard-sidebar {
position: fixed;
left: -280px;
top: 0;
bottom: 0;
width: 280px !important;
transition: left var(--transition-smooth);
box-shadow: var(--shadow-lg);
}
.dashboard-sidebar.mobile-open {
left: 0;
}
.sidebar-overlay {
display: block;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
z-index: 45;
}
}
+84
View File
@@ -0,0 +1,84 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { authService } from '../services/authService';
import { DEMO_USER } from '../services/mockData';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
const [loading, setLoading] = useState(true);
const [isDemoMode, setIsDemoMode] = useState(false);
useEffect(() => {
// Restore session on mount
try {
const storedToken = localStorage.getItem('authsec_token');
const storedUser = localStorage.getItem('authsec_user');
if (storedToken && storedUser) {
setToken(storedToken);
setUser(JSON.parse(storedUser));
}
} catch (e) {
console.error('Failed to parse stored session:', e);
localStorage.removeItem('authsec_token');
localStorage.removeItem('authsec_user');
} finally {
setLoading(false);
}
}, []);
const login = async (email, password, forceMock = false) => {
const res = await authService.login(email, password, forceMock);
const item = res.item || res;
const authToken = item.token || 'demo-token-' + Date.now();
// Store in localStorage
localStorage.setItem('authsec_token', authToken);
localStorage.setItem('authsec_user', JSON.stringify(item));
setToken(authToken);
setUser(item);
if (forceMock || res.operationMessage?.includes('Demo')) {
setIsDemoMode(true);
}
return item;
};
const loginAsDemo = async () => {
return login(DEMO_USER.email, 'admin123', true);
};
const logout = async () => {
await authService.logout();
setUser(null);
setToken(null);
setIsDemoMode(false);
};
const register = async (userData) => {
return authService.registerUser(userData);
};
const value = {
user,
token,
loading,
isAuthenticated: !!token && !!user,
isDemoMode,
login,
loginAsDemo,
logout,
register,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
+36
View File
@@ -0,0 +1,36 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() => {
const saved = localStorage.getItem('authsec_theme');
if (saved) return saved;
return window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches
? 'light'
: 'dark';
});
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('authsec_theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme, isDark: theme === 'dark' }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
+1
View File
@@ -0,0 +1 @@
@import './styles/global.css';
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
+331
View File
@@ -0,0 +1,331 @@
import React, { useState } from 'react';
import {
ShieldCheck,
Lock,
LayoutGrid,
Activity,
Key,
ShieldAlert,
CheckCircle,
MailCheck,
User,
Sliders,
FileText,
Copy,
ExternalLink,
Info,
} from 'lucide-react';
import { useAuth } from '../context/AuthContext';
import { DashboardLayout } from '../components/layout/DashboardLayout';
import { Badge } from '../components/common/Badge';
import { Button } from '../components/common/Button';
import { Card } from '../components/common/Card';
import { MOCK_STATS, MOCK_ACTIVITIES } from '../services/mockData';
import '../styles/dashboard.css';
export function Dashboard() {
const { user, token, isDemoMode, logout } = useAuth();
const [activeTab, setActiveTab] = useState('dashboard');
const [showTokenModal, setShowTokenModal] = useState(false);
const [copied, setCopied] = useState(false);
// Icon mapping for stats
const statIcons = {
ShieldCheck,
Lock,
LayoutGrid,
Activity,
};
// Icon mapping for activities
const activityIcons = {
Key,
ShieldAlert,
CheckCircle,
MailCheck,
};
const handleCopyToken = () => {
if (token) {
navigator.clipboard.writeText(token);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<DashboardLayout activeTab={activeTab} setActiveTab={setActiveTab}>
{/* Hero Welcome Banner */}
<section className="dashboard-hero animate-fade-in">
<div className="hero-left">
<div className="hero-tag">
<ShieldCheck size={16} />
<span>Identity & Access Controller</span>
</div>
<h1 className="hero-title">
Welcome back, <span className="gradient-text">{user?.firstName || 'Alex'}</span>
</h1>
<p className="hero-description">
AuthSec base project is operational. Your session is protected with Spring Boot JWT
authentication and granular role-based security boundaries.
</p>
</div>
<div className="hero-right">
<div className="hero-token-pill">
<Key size={14} />
<span>JWT: {token ? `${token.slice(0, 14)}...` : 'demo-token'}</span>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<Button
variant="secondary"
size="sm"
icon={Key}
onClick={() => setShowTokenModal(true)}
>
Inspect Token
</Button>
<Button
variant="outline"
size="sm"
onClick={logout}
>
Sign Out
</Button>
</div>
</div>
</section>
{/* Metric Stats Cards */}
<section className="stats-grid">
{MOCK_STATS.map((stat) => {
const Icon = statIcons[stat.icon] || Activity;
return (
<div key={stat.id} className="stat-card">
<div className="stat-card-header">
<span className="stat-card-title">{stat.title}</span>
<div className="stat-card-icon-box">
<Icon size={20} />
</div>
</div>
<div className="stat-card-value">{stat.value}</div>
<div className="stat-card-footer">
<span>{stat.subtitle}</span>
<span className={`stat-trend ${stat.status}`}>{stat.trend}</span>
</div>
</div>
);
})}
</section>
{/* Quick Action Buttons */}
<section>
<div className="section-header">
<h2 className="section-title">Quick Actions</h2>
</div>
<div className="quick-actions-grid">
<div className="quick-action-card" onClick={() => setShowTokenModal(true)}>
<div className="action-icon-circle">
<Key size={20} />
</div>
<div className="action-details">
<span className="action-name">Security Token</span>
<span className="action-desc">View JWT payload & auth headers</span>
</div>
</div>
<div
className="quick-action-card"
onClick={() => alert(`User Profile:\nName: ${user?.fullname || user?.firstName}\nEmail: ${user?.email}\nRoles: ${user?.roles?.join(', ')}`)}
>
<div className="action-icon-circle" style={{ background: 'linear-gradient(135deg, #06b6d4, #3b82f6)' }}>
<User size={20} />
</div>
<div className="action-details">
<span className="action-name">User Profile</span>
<span className="action-desc">Account ID #{user?.userId || '101'}</span>
</div>
</div>
<div
className="quick-action-card"
onClick={() => alert('System Parameters: Spring Boot Service Port 9292\nCORS: Enabled\nToken Lifetime: 24h')}
>
<div className="action-icon-circle" style={{ background: 'linear-gradient(135deg, #8b5cf6, #ec4899)' }}>
<Sliders size={20} />
</div>
<div className="action-details">
<span className="action-name">System Parameters</span>
<span className="action-desc">Endpoints & security policies</span>
</div>
</div>
<div
className="quick-action-card"
onClick={() => setActiveTab('security')}
>
<div className="action-icon-circle" style={{ background: 'linear-gradient(135deg, #10b981, #06b6d4)' }}>
<FileText size={20} />
</div>
<div className="action-details">
<span className="action-name">Audit Reports</span>
<span className="action-desc">Inspect security logs</span>
</div>
</div>
</div>
</section>
{/* Live Security Activity Stream */}
<section className="activity-card">
<div className="section-header">
<div>
<h2 className="section-title">Recent Security Events</h2>
<p style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>
Audited session entries recorded for this workspace
</p>
</div>
<Badge variant="primary" size="md">
Live Stream
</Badge>
</div>
<div className="activity-list">
{MOCK_ACTIVITIES.map((act) => {
const Icon = activityIcons[act.icon] || Key;
return (
<div key={act.id} className="activity-item">
<div className="activity-left">
<div className="activity-icon">
<Icon size={18} />
</div>
<div className="activity-info">
<span className="activity-title">{act.title}</span>
<span className="activity-desc">
{act.description} &bull; <strong style={{ color: 'var(--text-secondary)' }}>IP: {act.ip}</strong>
</span>
</div>
</div>
<div className="activity-right">
<Badge
variant={
act.badge === 'SUCCESS' ? 'success' : act.badge === 'SECURITY' ? 'warning' : 'info'
}
>
{act.badge}
</Badge>
<span className="activity-time">{act.timestamp}</span>
</div>
</div>
);
})}
</div>
</section>
{/* === BUILDER:TABS (do not remove) === */}
{/* Token Inspector Modal */}
{showTokenModal && (
<div
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0, 0, 0, 0.7)',
backdropFilter: 'blur(6px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: '20px',
}}
onClick={() => setShowTokenModal(false)}
>
<div
className="custom-card animate-fade-in"
style={{ maxWidth: '580px', width: '100%', maxHeight: '90vh', overflowY: 'auto' }}
onClick={(e) => e.stopPropagation()}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Key size={20} color="var(--color-primary-light)" />
<h3 style={{ fontSize: '1.25rem' }}>Session Token Inspector</h3>
</div>
<button
className="btn btn-ghost btn-sm"
onClick={() => setShowTokenModal(false)}
>
✕
</button>
</div>
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginBottom: '16px' }}>
This Bearer token is automatically attached to API requests in the <code>Authorization</code> header.
</p>
<div className="session-info-card">
<div className="session-info-row">
<span>Account Email:</span>
<strong>{user?.email || 'N/A'}</strong>
</div>
<div className="session-info-row">
<span>User ID:</span>
<strong>{user?.userId || '101'}</strong>
</div>
<div className="session-info-row">
<span>Assigned Roles:</span>
<strong>{Array.isArray(user?.roles) ? user.roles.join(', ') : 'ROLE_ADMIN'}</strong>
</div>
<div className="session-info-row">
<span>Mode:</span>
<strong>{isDemoMode ? 'Sandbox Demo Mode' : 'Live Spring Boot'}</strong>
</div>
</div>
<div style={{ marginTop: '16px' }}>
<label style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-secondary)' }}>
Raw JWT Token:
</label>
<textarea
readOnly
value={token || ''}
style={{
width: '100%',
height: '80px',
background: 'var(--bg-input)',
color: 'var(--text-main)',
border: '1px solid var(--border-subtle)',
borderRadius: 'var(--radius-sm)',
padding: '10px',
fontSize: '0.75rem',
fontFamily: 'monospace',
resize: 'none',
marginTop: '6px',
}}
/>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px', marginTop: '20px' }}>
<Button
variant="secondary"
size="sm"
icon={Copy}
onClick={handleCopyToken}
>
{copied ? 'Copied to Clipboard!' : 'Copy Token'}
</Button>
<Button
variant="primary"
size="sm"
onClick={() => setShowTokenModal(false)}
>
Done
</Button>
</div>
</div>
</div>
)}
</DashboardLayout>
);
}
+172
View File
@@ -0,0 +1,172 @@
import React, { useState } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { Shield, Mail, Lock, Sparkles, ArrowRight, Server } from 'lucide-react';
import { useAuth } from '../context/AuthContext';
import { Button } from '../components/common/Button';
import { Input } from '../components/common/Input';
import { Toast } from '../components/common/Toast';
import { ThemeToggle } from '../components/layout/ThemeToggle';
import { DEMO_USER } from '../services/mockData';
import '../styles/auth.css';
export function Login() {
const navigate = useNavigate();
const location = useLocation();
const { login } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [rememberMe, setRememberMe] = useState(true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// If redirected with registered email
React.useEffect(() => {
if (location.state?.registeredEmail) {
setEmail(location.state.registeredEmail);
}
}, [location.state]);
const handleSubmit = async (e) => {
e.preventDefault();
if (!email.trim() || !password.trim()) {
setError('Please provide both email and password.');
return;
}
try {
setLoading(true);
setError(null);
await login(email, password);
navigate('/dashboard');
} catch (err) {
console.error('Login error:', err);
setError(err.message || 'Failed to authenticate. Please check your credentials.');
} finally {
setLoading(false);
}
};
const handleQuickDemo = async () => {
setEmail(DEMO_USER.email);
setPassword('admin123');
try {
setLoading(true);
setError(null);
await login(DEMO_USER.email, 'admin123', true);
navigate('/dashboard');
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div className="auth-container">
{/* Background Orbs */}
<div className="auth-bg-decorations">
<div className="auth-orb auth-orb-1" />
<div className="auth-orb auth-orb-2" />
<div className="auth-orb auth-orb-3" />
</div>
<div className="auth-card animate-fade-in">
{/* Top bar with Brand and Theme Toggle */}
<div className="auth-top-bar">
<div className="auth-brand">
<div className="auth-brand-icon">
<Shield size={22} />
</div>
<span className="auth-brand-name">AuthSec</span>
</div>
<ThemeToggle />
</div>
{/* Header */}
<div className="auth-header">
<h1 className="auth-title">Welcome back</h1>
<p className="auth-subtitle">Sign in to your enterprise security console</p>
</div>
{/* Demo Credentials Quick-Fill Banner */}
<div className="demo-quick-bar">
<div className="demo-quick-info">
<Sparkles size={16} />
<span>Demo: <strong>{DEMO_USER.email}</strong></span>
</div>
<button
type="button"
className="demo-quick-btn"
onClick={handleQuickDemo}
disabled={loading}
>
One-Click Login
</button>
</div>
{/* Error Toast */}
{error && <Toast type="error" message={error} onClose={() => setError(null)} />}
{/* Login Form */}
<form onSubmit={handleSubmit} className="auth-form" noValidate>
<Input
label="Email Address"
type="email"
placeholder="admin@authsec.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
icon={Mail}
required
autoComplete="email"
/>
<Input
label="Password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
icon={Lock}
required
autoComplete="current-password"
/>
<div className="auth-options">
<label className="auth-remember">
<input
type="checkbox"
className="auth-checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
/>
<span>Remember this session</span>
</label>
<a href="#trouble" className="auth-link" onClick={(e) => { e.preventDefault(); setError('Contact security administrator to reset credential tokens.'); }}>
Forgot password?
</a>
</div>
<Button
type="submit"
variant="primary"
size="lg"
fullWidth
isLoading={loading}
icon={ArrowRight}
>
Sign In to AuthSec
</Button>
</form>
{/* Bottom Switch to Register */}
<div className="auth-footer">
Don't have an enterprise account?{' '}
<Link to="/register" className="auth-link">
Register here
</Link>
</div>
</div>
</div>
);
}
+48
View File
@@ -0,0 +1,48 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ShieldAlert, Home } from 'lucide-react';
import { Button } from '../components/common/Button';
export function NotFound() {
return (
<div
style={{
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '24px',
textAlign: 'center',
background: 'var(--bg-app)',
}}
>
<div
style={{
width: 64,
height: 64,
borderRadius: 'var(--radius-lg)',
background: 'var(--color-danger-bg)',
color: 'var(--color-danger)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '20px',
}}
>
<ShieldAlert size={36} />
</div>
<h1 style={{ fontSize: '2.5rem', marginBottom: '8px' }}>404 - Not Found</h1>
<p style={{ color: 'var(--text-secondary)', maxWidth: '420px', marginBottom: '24px' }}>
The security boundary or requested resource does not exist or has been relocated.
</p>
<Link to="/dashboard">
<Button variant="primary" icon={Home}>
Return to Dashboard
</Button>
</Link>
</div>
);
}
+415
View File
@@ -0,0 +1,415 @@
import React, { useState, useRef } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Shield, Mail, Lock, User, Phone, ArrowRight, ArrowLeft, CheckCircle2, RefreshCw } from 'lucide-react';
import { authService } from '../services/authService';
import { Button } from '../components/common/Button';
import { Input } from '../components/common/Input';
import { Toast } from '../components/common/Toast';
import { ThemeToggle } from '../components/layout/ThemeToggle';
import '../styles/auth.css';
export function Register() {
const navigate = useNavigate();
// Wizard Steps: 1 = Email OTP, 2 = Verify OTP, 3 = Profile Details
const [currentStep, setCurrentStep] = useState(1);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [successMsg, setSuccessMsg] = useState(null);
// Form State
const [email, setEmail] = useState('');
const [otp, setOtp] = useState(['', '', '', '', '', '']);
const [resendTimer, setResendTimer] = useState(60);
const [canResend, setCanResend] = useState(false);
// Profile Details State
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
phone: '',
password: '',
confirmPassword: '',
});
const otpInputRefs = useRef([]);
// Timer countdown for OTP resend
React.useEffect(() => {
let interval = null;
if (currentStep === 2 && resendTimer > 0) {
interval = setInterval(() => {
setResendTimer((prev) => {
if (prev <= 1) {
setCanResend(true);
return 0;
}
return prev - 1;
});
}, 1000);
}
return () => clearInterval(interval);
}, [currentStep, resendTimer]);
// Step 1: Send OTP
const handleSendOtp = async (e) => {
e.preventDefault();
if (!email.trim() || !email.includes('@')) {
setError('Please provide a valid email address.');
return;
}
try {
setLoading(true);
setError(null);
const res = await authService.sendOtp(email);
setSuccessMsg(res.mockOtp ? `Verification code sent! (Demo OTP: ${res.mockOtp})` : 'Verification code sent to your email.');
setCurrentStep(2);
setResendTimer(60);
setCanResend(false);
} catch (err) {
setError(err.message || 'Failed to send verification OTP.');
} finally {
setLoading(false);
}
};
// Step 2: Handle OTP box input & verification
const handleOtpChange = (index, value) => {
if (value.length > 1) {
// Handle paste
const pastedDigits = value.replace(/\D/g, '').slice(0, 6).split('');
const newOtp = [...otp];
pastedDigits.forEach((digit, i) => {
newOtp[i] = digit;
});
setOtp(newOtp);
const nextIndex = Math.min(pastedDigits.length, 5);
otpInputRefs.current[nextIndex]?.focus();
return;
}
const newOtp = [...otp];
newOtp[index] = value;
setOtp(newOtp);
// Auto-focus next input
if (value && index < 5) {
otpInputRefs.current[index + 1]?.focus();
}
};
const handleOtpKeyDown = (index, e) => {
if (e.key === 'Backspace' && !otp[index] && index > 0) {
otpInputRefs.current[index - 1]?.focus();
}
};
const handleVerifyOtp = async (e) => {
e.preventDefault();
const code = otp.join('');
if (code.length < 6) {
setError('Please enter the complete 6-digit verification code.');
return;
}
try {
setLoading(true);
setError(null);
await authService.verifyOtp(email, code);
setSuccessMsg('Email verified successfully! Complete your account details.');
setCurrentStep(3);
} catch (err) {
setError(err.message || 'Invalid or expired OTP. (Demo code: 123456)');
} finally {
setLoading(false);
}
};
const handleResend = async () => {
if (!canResend) return;
try {
setLoading(true);
setError(null);
const res = await authService.resendOtp(email);
setSuccessMsg(res.mockOtp ? `New code sent! (Demo OTP: ${res.mockOtp})` : 'New verification code sent.');
setResendTimer(60);
setCanResend(false);
} catch (err) {
setError(err.message || 'Failed to resend OTP.');
} finally {
setLoading(false);
}
};
// Step 3: Complete Account Registration
const handleCompleteRegistration = async (e) => {
e.preventDefault();
if (!formData.firstName.trim() || !formData.lastName.trim()) {
setError('Please provide your first and last name.');
return;
}
if (formData.password.length < 6) {
setError('Password must be at least 6 characters.');
return;
}
if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match.');
return;
}
try {
setLoading(true);
setError(null);
await authService.registerUser({
email,
firstName: formData.firstName,
lastName: formData.lastName,
phone: formData.phone,
password: formData.password,
});
// Navigate to login with registered email
navigate('/login', {
state: { registeredEmail: email },
});
} catch (err) {
setError(err.message || 'Failed to create account.');
} finally {
setLoading(false);
}
};
return (
<div className="auth-container">
{/* Background Orbs */}
<div className="auth-bg-decorations">
<div className="auth-orb auth-orb-1" />
<div className="auth-orb auth-orb-2" />
<div className="auth-orb auth-orb-3" />
</div>
<div className="auth-card animate-fade-in">
{/* Top bar with Brand and Theme Toggle */}
<div className="auth-top-bar">
<div className="auth-brand">
<div className="auth-brand-icon">
<Shield size={22} />
</div>
<span className="auth-brand-name">AuthSec</span>
</div>
<ThemeToggle />
</div>
{/* Wizard Stepper */}
<div className="register-stepper">
<div className={`stepper-step ${currentStep === 1 ? 'active' : currentStep > 1 ? 'completed' : ''}`}>
<div className="stepper-badge">{currentStep > 1 ? '✓' : '1'}</div>
<span className="stepper-title">Email</span>
</div>
<div className={`stepper-step ${currentStep === 2 ? 'active' : currentStep > 2 ? 'completed' : ''}`}>
<div className="stepper-badge">{currentStep > 2 ? '✓' : '2'}</div>
<span className="stepper-title">Verify OTP</span>
</div>
<div className={`stepper-step ${currentStep === 3 ? 'active' : ''}`}>
<div className="stepper-badge">3</div>
<span className="stepper-title">Profile</span>
</div>
</div>
{/* Header */}
<div className="auth-header">
<h1 className="auth-title">
{currentStep === 1 && 'Create an account'}
{currentStep === 2 && 'Verify your email'}
{currentStep === 3 && 'Account Details'}
</h1>
<p className="auth-subtitle">
{currentStep === 1 && 'Register for enterprise security workspace'}
{currentStep === 2 && `Enter the 6-digit code sent to ${email}`}
{currentStep === 3 && 'Set up your credentials and personal profile'}
</p>
</div>
{/* Notifications */}
{error && <Toast type="error" message={error} onClose={() => setError(null)} />}
{successMsg && <Toast type="success" message={successMsg} onClose={() => setSuccessMsg(null)} />}
{/* STEP 1: Email Input */}
{currentStep === 1 && (
<form onSubmit={handleSendOtp} className="auth-form" noValidate>
<Input
label="Work Email"
type="email"
placeholder="user@enterprise.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
icon={Mail}
required
helperText="We will send a one-time verification code to this address."
/>
<Button
type="submit"
variant="primary"
size="lg"
fullWidth
isLoading={loading}
icon={ArrowRight}
>
Send Verification Code
</Button>
{/* Direct Skip option for quick testing */}
<button
type="button"
className="btn btn-ghost btn-sm"
style={{ marginTop: '-6px' }}
onClick={() => {
if (!email) setEmail('test@authsec.com');
setCurrentStep(3);
}}
>
Skip OTP (Direct Registration) →
</button>
</form>
)}
{/* STEP 2: OTP Verification */}
{currentStep === 2 && (
<form onSubmit={handleVerifyOtp} className="auth-form" noValidate>
<div className="otp-inputs">
{otp.map((digit, idx) => (
<input
key={idx}
ref={(el) => (otpInputRefs.current[idx] = el)}
type="text"
maxLength={1}
className="otp-box"
value={digit}
onChange={(e) => handleOtpChange(idx, e.target.value)}
onKeyDown={(e) => handleOtpKeyDown(idx, e)}
autoFocus={idx === 0}
inputMode="numeric"
/>
))}
</div>
<div className="otp-resend-row">
<button
type="button"
className="otp-resend-btn"
onClick={handleResend}
disabled={!canResend || loading}
>
{canResend ? 'Resend verification code' : `Resend code in ${resendTimer}s`}
</button>
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={() => setCurrentStep(1)}
>
Change Email
</button>
</div>
<Button
type="submit"
variant="primary"
size="lg"
fullWidth
isLoading={loading}
icon={CheckCircle2}
>
Verify & Proceed
</Button>
</form>
)}
{/* STEP 3: Profile Details */}
{currentStep === 3 && (
<form onSubmit={handleCompleteRegistration} className="auth-form" noValidate>
<div className="form-row">
<Input
label="First Name"
placeholder="Jane"
value={formData.firstName}
onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
icon={User}
required
/>
<Input
label="Last Name"
placeholder="Doe"
value={formData.lastName}
onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
icon={User}
required
/>
</div>
<Input
label="Phone Number"
type="tel"
placeholder="+1 (555) 000-0000"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
icon={Phone}
/>
<Input
label="Password"
type="password"
placeholder="••••••••"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
icon={Lock}
required
helperText="Minimum 6 characters"
/>
<Input
label="Confirm Password"
type="password"
placeholder="••••••••"
value={formData.confirmPassword}
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
icon={Lock}
required
/>
<div style={{ display: 'flex', gap: '10px' }}>
<Button
type="button"
variant="secondary"
size="lg"
icon={ArrowLeft}
onClick={() => setCurrentStep(2)}
>
Back
</Button>
<Button
type="submit"
variant="primary"
size="lg"
fullWidth
isLoading={loading}
icon={CheckCircle2}
>
Complete Registration
</Button>
</div>
</form>
)}
{/* Footer */}
<div className="auth-footer">
Already have an account?{' '}
<Link to="/login" className="auth-link">
Sign In
</Link>
</div>
</div>
</div>
);
}
+53
View File
@@ -0,0 +1,53 @@
const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:9292';
/**
* Make an HTTP request with automatic token header injection and error handling.
*/
export async function request(endpoint, options = {}) {
const token = localStorage.getItem('authsec_token');
const headers = {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers,
};
const url = endpoint.startsWith('http') ? endpoint : `${BASE_URL}${endpoint}`;
try {
const response = await fetch(url, {
...options,
headers,
});
const contentType = response.headers.get('content-type');
let data;
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
if (!response.ok) {
const errorMessage =
(typeof data === 'object' && (data.operationMessage || data.message || data.error)) ||
(typeof data === 'string' && data) ||
`HTTP Error: ${response.status}`;
const err = new Error(errorMessage);
err.status = response.status;
err.data = data;
throw err;
}
return data;
} catch (error) {
// If network fetch fails completely (e.g. backend server is not running)
if (error.name === 'TypeError' && error.message.includes('fetch')) {
const netErr = new Error('Cannot reach Spring Boot server at ' + BASE_URL + '. Is backend running?');
netErr.isNetworkError = true;
throw netErr;
}
throw error;
}
}
+200
View File
@@ -0,0 +1,200 @@
import { request } from './api';
import { DEMO_USER } from './mockData';
export const authService = {
/**
* Log into Spring Boot backend (/token/session)
*/
async login(email, password, useMockFallback = false) {
if (useMockFallback) {
// Simulate slight network delay for realistic UX
await new Promise((r) => setTimeout(r, 600));
return {
operationStatus: 'SUCCESS',
operationMessage: 'Demo session authenticated',
item: {
...DEMO_USER,
email: email || DEMO_USER.email,
},
};
}
try {
const response = await request('/token/session', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
if (response.operationStatus === 'ERROR') {
throw new Error(response.operationMessage || 'Login failed');
}
return response;
} catch (err) {
if (err.isNetworkError) {
// If server is not running and user enters demo account, offer seamless demo fallback
if (email.toLowerCase().includes('admin') || email.toLowerCase().includes('demo')) {
console.warn('Backend server offline. Falling back to Demo mode for admin account.');
return {
operationStatus: 'SUCCESS',
operationMessage: 'Connected in Demo Mode (Backend Offline)',
item: {
...DEMO_USER,
email,
},
};
}
}
throw err;
}
},
/**
* Send OTP for Email Verification (/token/user/send_email)
*/
async sendOtp(email) {
try {
const res = await request('/token/user/send_email', {
method: 'POST',
body: JSON.stringify({ email }),
});
return res;
} catch (err) {
if (err.isNetworkError) {
// Mock fallback for testing OTP flow without active SMTP
console.warn('Backend offline - Mock OTP 123456 generated');
return { message: 'Demo OTP sent: 123456', mockOtp: '123456' };
}
throw err;
}
},
/**
* Resend OTP (/token/user/resend_otp?email=...)
*/
async resendOtp(email) {
try {
const res = await request(`/token/user/resend_otp?email=${encodeURIComponent(email)}`, {
method: 'POST',
});
return res;
} catch (err) {
if (err.isNetworkError) {
return { message: 'Demo OTP resent: 123456', mockOtp: '123456' };
}
throw err;
}
},
/**
* Verify OTP (/token/user/otp_verification?email=...&otp=...)
*/
async verifyOtp(email, otp) {
// If testing mock OTP
if (otp === '123456') {
return { status: 'OK', message: 'OTP Verified' };
}
try {
const res = await request(
`/token/user/otp_verification?email=${encodeURIComponent(email)}&otp=${encodeURIComponent(otp)}`,
{ method: 'POST' }
);
return res;
} catch (err) {
if (err.isNetworkError) {
if (otp.length === 6) {
return { status: 'OK', message: 'Demo OTP Verified' };
}
throw new Error('Invalid OTP code. Try 123456 for demo.');
}
throw err;
}
},
/**
* Create sysaccount (/token/users/sysaccount/savesysaccount)
*/
async createAccount(email) {
try {
const res = await request('/token/users/sysaccount/savesysaccount', {
method: 'POST',
body: JSON.stringify({ email }),
});
return res;
} catch (e) {
console.warn('Account creation step note:', e);
return null;
}
},
/**
* Register user (/token/addOneAppUser)
* Strictly formats payload to the 12 fields expected by com.realnet.users.entity1.Registration
*/
async registerUser(userData) {
// 1. Resolve or create account_id
let accountId = userData.account_id;
if (!accountId) {
try {
const accountRes = await this.createAccount(userData.email);
accountId = accountRes?.account_id || accountRes?.id || 1;
} catch (err) {
accountId = 1;
}
}
// 2. Parse phone/mobile to Long number
const mobDigits = String(userData.phone || userData.mob_no || '9876543210').replace(/\D/g, '');
const mob_no = mobDigits ? parseInt(mobDigits.slice(-10), 10) : 9876543210;
// 3. Exact 12 fields for Spring Boot Registration entity
const registrationPayload = {
first_name: userData.first_name || userData.firstName || '',
last_name: userData.last_name || userData.lastName || '',
email: userData.email,
mob_no: mob_no,
account_id: Number(accountId) || 1,
usrGrpId: Number(userData.usrGrpId) || 1,
new_password: userData.new_password || userData.password || '',
confirm_password: userData.confirm_password || userData.confirmPassword || userData.password || '',
accesstype: userData.accesstype || 'Regular',
gender: userData.gender || 'Not Specified',
date_of_birth: userData.date_of_birth || userData.dob || '2000-01-01',
how_you_know_aboutus: userData.how_you_know_aboutus || 'Direct',
};
try {
const res = await request('/token/addOneAppUser', {
method: 'POST',
body: JSON.stringify(registrationPayload),
});
return res;
} catch (err) {
if (err.isNetworkError) {
// Mock fallback
return {
userId: Date.now(),
...registrationPayload,
message: 'Account created successfully (Demo Mode)',
};
}
throw err;
}
},
/**
* Logout (/token/logout)
*/
async logout() {
try {
await request('/token/logout', { method: 'GET' });
} catch (e) {
// Ignore logout backend error and continue local cleanup
console.log('Backend logout response or offline:', e);
} finally {
localStorage.removeItem('authsec_token');
localStorage.removeItem('authsec_user');
}
},
};
+103
View File
@@ -0,0 +1,103 @@
// Pre-configured mock data matching authsec_springboot schema
export const DEMO_USER = {
userId: 101,
firstName: "Alex",
lastName: "Vance",
fullname: "Alex Vance",
username: "alex.admin",
email: "admin@authsec.com",
phone: "+1 (555) 234-5678",
roles: ["ROLE_ADMIN", "SECURITY_OFFICER", "WORKSPACE_MANAGER"],
token: "eyJhbGciOiJIUzUxMiJ9.authsec_demo_token_authenticated_user_session_9942",
avatar: "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=256&q=80",
lastLogin: new Date().toISOString(),
account: {
accountId: 4001,
name: "Enterprise Security Core",
plan: "Enterprise Shield",
status: "ACTIVE"
}
};
export const MOCK_STATS = [
{
id: "sessions",
title: "Active Sessions",
value: "1",
subtitle: "Current device IP: 127.0.0.1",
trend: "+0 this week",
status: "positive",
icon: "ShieldCheck"
},
{
id: "securityScore",
title: "Security Posture",
value: "98.4%",
subtitle: "JWT 512-bit & Role Guard enabled",
trend: "+2.1% optimal",
status: "positive",
icon: "Lock"
},
{
id: "workspaces",
title: "Managed Workspaces",
value: "4",
subtitle: "Production, Staging, QA, Dev",
trend: "All synced",
status: "neutral",
icon: "LayoutGrid"
},
{
id: "apiGateway",
title: "API Gateway",
value: "Healthy",
subtitle: "Spring Boot Port :9292",
trend: "24ms response",
status: "positive",
icon: "Activity"
}
];
export const MOCK_ACTIVITIES = [
{
id: "act-1",
type: "LOGIN_SUCCESS",
title: "Successful Session Authentication",
description: "Authenticated via Spring Boot /token/session endpoint",
timestamp: "Just now",
ip: "127.0.0.1",
badge: "SUCCESS",
icon: "Key"
},
{
id: "act-2",
type: "TOKEN_GENERATED",
title: "JWT Access Token Issued",
description: "Session authorization header applied with HMAC-SHA512",
timestamp: "2 mins ago",
ip: "127.0.0.1",
badge: "SECURITY",
icon: "ShieldAlert"
},
{
id: "act-3",
type: "WORKSPACE_SYNC",
title: "Security Workspace Handshake",
description: "Role mappings verified: ROLE_ADMIN, WORKSPACE_MANAGER",
timestamp: "12 mins ago",
ip: "10.0.2.15",
badge: "INFO",
icon: "CheckCircle"
},
{
id: "act-4",
type: "OTP_VERIFIED",
title: "Email Verification Completed",
description: "Two-step OTP challenge validated successfully",
timestamp: "1 hour ago",
ip: "192.168.1.104",
badge: "VERIFIED",
icon: "MailCheck"
}
];
+350
View File
@@ -0,0 +1,350 @@
/* Auth Pages CSS */
.auth-container {
min-height: 100vh;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
padding: 24px;
}
/* Background animated radiant orbs */
.auth-bg-decorations {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
z-index: 0;
}
.auth-orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.45;
animation: floatOrbs 18s ease-in-out infinite alternate;
}
.auth-orb-1 {
width: 480px;
height: 480px;
background: radial-gradient(circle, rgba(99, 102, 241, 0.6), transparent 70%);
top: -100px;
left: -100px;
}
.auth-orb-2 {
width: 420px;
height: 420px;
background: radial-gradient(circle, rgba(6, 182, 212, 0.5), transparent 70%);
bottom: -80px;
right: -80px;
animation-delay: -5s;
}
.auth-orb-3 {
width: 320px;
height: 320px;
background: radial-gradient(circle, rgba(139, 92, 246, 0.4), transparent 70%);
top: 40%;
right: 25%;
animation-delay: -9s;
}
/* Auth Card */
.auth-card {
width: 100%;
max-width: 460px;
position: relative;
z-index: 10;
background: var(--bg-glass-card);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-xl);
padding: 40px 36px;
box-shadow: var(--shadow-lg), 0 0 50px rgba(99, 102, 241, 0.12);
transition: all var(--transition-normal);
}
.auth-card:hover {
border-color: rgba(99, 102, 241, 0.25);
}
/* Auth Top Bar */
.auth-top-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
}
.auth-brand {
display: flex;
align-items: center;
gap: 10px;
}
.auth-brand-icon {
width: 40px;
height: 40px;
border-radius: var(--radius-md);
background: var(--gradient-primary);
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
}
.auth-brand-name {
font-size: 1.35rem;
font-weight: 800;
letter-spacing: -0.03em;
}
/* Auth Header */
.auth-header {
margin-bottom: 28px;
text-align: left;
}
.auth-title {
font-size: 1.85rem;
font-weight: 700;
margin-bottom: 6px;
line-height: 1.2;
}
.auth-subtitle {
font-size: 0.925rem;
color: var(--text-secondary);
}
/* Quick Demo Bar */
.demo-quick-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(99, 102, 241, 0.08);
border: 1px dashed rgba(99, 102, 241, 0.35);
border-radius: var(--radius-md);
padding: 10px 14px;
margin-bottom: 22px;
}
.demo-quick-info {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8rem;
color: var(--color-primary-light);
}
.demo-quick-btn {
font-size: 0.775rem;
font-weight: 600;
color: #ffffff;
background: var(--color-primary);
padding: 4px 10px;
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
}
.demo-quick-btn:hover {
background: var(--color-primary-hover);
transform: translateY(-1px);
}
/* Auth Form */
.auth-form {
display: flex;
flex-direction: column;
gap: 18px;
}
.auth-options {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.85rem;
color: var(--text-secondary);
margin-top: -4px;
}
.auth-remember {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
}
.auth-checkbox {
accent-color: var(--color-primary);
width: 16px;
height: 16px;
border-radius: 4px;
cursor: pointer;
}
.auth-link {
color: var(--color-primary-light);
font-weight: 500;
}
.auth-link:hover {
text-decoration: underline;
}
/* Footer Switch */
.auth-footer {
margin-top: 28px;
text-align: center;
font-size: 0.9rem;
color: var(--text-secondary);
border-top: 1px solid var(--border-subtle);
padding-top: 20px;
}
/* Step Wizard for Registration */
.register-stepper {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
position: relative;
}
.register-stepper::before {
content: '';
position: absolute;
top: 50%;
left: 15%;
right: 15%;
height: 2px;
background: var(--border-subtle);
z-index: 1;
transform: translateY(-50%);
}
.stepper-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
position: relative;
z-index: 2;
}
.stepper-badge {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.85rem;
background: var(--bg-surface-elevated);
border: 2px solid var(--border-subtle);
color: var(--text-muted);
transition: all var(--transition-normal);
}
.stepper-step.active .stepper-badge {
background: var(--color-primary);
border-color: var(--color-primary);
color: #ffffff;
box-shadow: 0 0 16px rgba(99, 102, 241, 0.45);
}
.stepper-step.completed .stepper-badge {
background: var(--color-success);
border-color: var(--color-success);
color: #ffffff;
}
.stepper-title {
font-size: 0.725rem;
font-weight: 600;
color: var(--text-muted);
}
.stepper-step.active .stepper-title {
color: var(--text-main);
}
/* OTP Code Input Grid */
.otp-inputs {
display: flex;
justify-content: space-between;
gap: 10px;
margin: 16px 0;
}
.otp-box {
width: 48px;
height: 54px;
text-align: center;
font-size: 1.5rem;
font-weight: 700;
font-family: var(--font-heading);
background: var(--bg-input);
color: var(--text-main);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
outline: none;
transition: all var(--transition-normal);
}
.otp-box:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.25);
}
.otp-resend-row {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.825rem;
color: var(--text-secondary);
margin-top: 6px;
}
.otp-resend-btn {
color: var(--color-primary-light);
font-weight: 600;
cursor: pointer;
background: none;
border: none;
}
.otp-resend-btn:disabled {
color: var(--text-muted);
cursor: not-allowed;
}
/* Grid form row */
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
@media (max-width: 480px) {
.auth-card {
padding: 28px 20px;
}
.form-row {
grid-template-columns: 1fr;
}
.otp-box {
width: 40px;
height: 48px;
font-size: 1.25rem;
}
}
+349
View File
@@ -0,0 +1,349 @@
/* Dashboard Styles */
.dashboard-content-area {
padding: 32px;
flex: 1;
display: flex;
flex-direction: column;
gap: 28px;
max-width: 1400px;
margin: 0 auto;
width: 100%;
}
/* Welcome Hero Banner */
.dashboard-hero {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(6, 182, 212, 0.08) 50%, rgba(14, 20, 36, 0.4) 100%);
border: 1px solid rgba(99, 102, 241, 0.25);
border-radius: var(--radius-xl);
padding: 32px;
position: relative;
overflow: hidden;
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
}
[data-theme='light'] .dashboard-hero {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.12) 0%, rgba(6, 182, 212, 0.06) 100%);
}
.hero-left {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
gap: 6px;
}
.hero-tag {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.775rem;
font-weight: 700;
color: var(--color-primary-light);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.hero-title {
font-size: 2rem;
font-weight: 800;
line-height: 1.2;
}
.hero-description {
font-size: 0.95rem;
color: var(--text-secondary);
max-width: 600px;
}
.hero-right {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
}
.hero-token-pill {
background: var(--bg-surface-elevated);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-full);
padding: 6px 14px;
font-size: 0.8rem;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 8px;
font-family: monospace;
}
/* Stats Cards Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 20px;
}
.stat-card {
background: var(--bg-glass-card);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
padding: 22px;
display: flex;
flex-direction: column;
gap: 14px;
transition: all var(--transition-normal);
position: relative;
overflow: hidden;
}
.stat-card:hover {
transform: translateY(-3px);
border-color: rgba(99, 102, 241, 0.35);
box-shadow: var(--shadow-lg), var(--shadow-glow);
}
.stat-card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.stat-card-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-secondary);
}
.stat-card-icon-box {
width: 40px;
height: 40px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
background: rgba(99, 102, 241, 0.12);
color: var(--color-primary-light);
}
.stat-card-value {
font-size: 1.85rem;
font-weight: 800;
line-height: 1.1;
font-family: var(--font-heading);
}
.stat-card-footer {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.775rem;
border-top: 1px solid var(--border-subtle);
padding-top: 10px;
color: var(--text-muted);
}
.stat-trend {
font-weight: 600;
}
.stat-trend.positive {
color: var(--color-success);
}
/* Quick Actions Section */
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.section-title {
font-size: 1.25rem;
font-weight: 700;
}
.quick-actions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.quick-action-card {
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
padding: 18px;
display: flex;
align-items: center;
gap: 14px;
cursor: pointer;
transition: all var(--transition-normal);
}
.quick-action-card:hover {
background: var(--bg-surface-elevated);
border-color: var(--border-glow);
transform: translateY(-2px);
}
.action-icon-circle {
width: 44px;
height: 44px;
border-radius: var(--radius-md);
background: var(--gradient-primary);
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
flex-shrink: 0;
}
.action-details {
display: flex;
flex-direction: column;
}
.action-name {
font-weight: 700;
font-size: 0.925rem;
color: var(--text-main);
}
.action-desc {
font-size: 0.775rem;
color: var(--text-muted);
}
/* Activity Table / Stream */
.activity-card {
background: var(--bg-glass-card);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
padding: 24px;
overflow: hidden;
}
.activity-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.activity-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-radius: var(--radius-md);
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
transition: all var(--transition-fast);
}
.activity-item:hover {
border-color: rgba(99, 102, 241, 0.25);
background: var(--bg-surface-elevated);
}
.activity-left {
display: flex;
align-items: center;
gap: 14px;
}
.activity-icon {
width: 38px;
height: 38px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(99, 102, 241, 0.12);
color: var(--color-primary-light);
flex-shrink: 0;
}
.activity-info {
display: flex;
flex-direction: column;
}
.activity-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-main);
}
.activity-desc {
font-size: 0.775rem;
color: var(--text-muted);
}
.activity-right {
display: flex;
align-items: center;
gap: 16px;
}
.activity-time {
font-size: 0.775rem;
color: var(--text-muted);
}
/* Security Session Drawer/Modal Preview */
.session-info-card {
background: var(--bg-surface-elevated);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
padding: 16px;
display: flex;
flex-direction: column;
gap: 10px;
font-size: 0.825rem;
}
.session-info-row {
display: flex;
justify-content: space-between;
color: var(--text-secondary);
}
.session-info-row strong {
color: var(--text-main);
font-family: monospace;
}
@media (max-width: 768px) {
.dashboard-content-area {
padding: 18px;
gap: 20px;
}
.dashboard-hero {
flex-direction: column;
align-items: flex-start;
padding: 22px;
}
.hero-right {
align-items: flex-start;
}
.activity-item {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.activity-right {
width: 100%;
justify-content: space-between;
}
}
+144
View File
@@ -0,0 +1,144 @@
@import './variables.css';
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
min-height: 100%;
font-family: var(--font-body);
background-color: var(--bg-app);
color: var(--text-main);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background-color var(--transition-normal), color var(--transition-normal);
overflow-x: hidden;
}
#root {
min-height: 100vh;
display: flex;
flex-direction: column;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
font-weight: 700;
color: var(--text-main);
letter-spacing: -0.02em;
}
a {
color: var(--color-primary-light);
text-decoration: none;
transition: color var(--transition-fast);
}
a:hover {
color: var(--color-primary);
}
button {
font-family: var(--font-heading);
border: none;
background: none;
cursor: pointer;
outline: none;
}
input, select, textarea {
font-family: var(--font-body);
}
/* Scrollbars */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-surface);
}
::-webkit-scrollbar-thumb {
background: var(--border-subtle);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulseSlow {
0%, 100% {
transform: scale(1);
opacity: 0.8;
}
50% {
transform: scale(1.08);
opacity: 1;
}
}
@keyframes floatOrbs {
0% {
transform: translate(0, 0) rotate(0deg);
}
50% {
transform: translate(25px, 20px) rotate(180deg);
}
100% {
transform: translate(0, 0) rotate(360deg);
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-fade-in {
animation: fadeIn 0.4s var(--transition-smooth) forwards;
}
/* Glassmorphism helpers */
.glass-panel {
background: var(--bg-glass-card);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
}
.glass-panel:hover {
border-color: rgba(99, 102, 241, 0.25);
}
/* Text Gradients */
.gradient-text {
background: var(--gradient-primary);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: inline-block;
}
+91
View File
@@ -0,0 +1,91 @@
:root {
/* Fonts */
--font-heading: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-body: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
/* Brand Accents */
--color-primary: #6366f1;
--color-primary-rgb: 99, 102, 241;
--color-primary-hover: #4f46e5;
--color-primary-light: #818cf8;
--color-secondary: #06b6d4;
--color-secondary-rgb: 6, 182, 212;
--color-accent-purple: #8b5cf6;
--color-accent-pink: #ec4899;
/* Status Colors */
--color-success: #10b981;
--color-success-bg: rgba(16, 185, 129, 0.12);
--color-warning: #f59e0b;
--color-warning-bg: rgba(245, 158, 11, 0.12);
--color-danger: #ef4444;
--color-danger-bg: rgba(239, 68, 68, 0.12);
--color-info: #3b82f6;
--color-info-bg: rgba(59, 130, 246, 0.12);
/* Gradients */
--gradient-primary: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #06b6d4 100%);
--gradient-glow: radial-gradient(circle at 50% 50%, rgba(99, 102, 241, 0.25) 0%, rgba(6, 182, 212, 0.05) 50%, transparent 80%);
--gradient-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.01) 100%);
/* Transitions & Radii */
--radius-xs: 4px;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 24px;
--radius-full: 9999px;
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-smooth: 350ms cubic-bezier(0.16, 1, 0.3, 1);
}
/* Dark Theme (Default) */
[data-theme='dark'],
:root {
--bg-app: #080c14;
--bg-surface: #0e1424;
--bg-surface-elevated: #141c30;
--bg-glass: rgba(14, 20, 36, 0.72);
--bg-glass-card: rgba(18, 25, 44, 0.65);
--bg-input: rgba(14, 20, 36, 0.85);
--border-subtle: rgba(255, 255, 255, 0.07);
--border-glow: rgba(99, 102, 241, 0.35);
--border-focus: rgba(99, 102, 241, 0.8);
--text-main: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--text-inverse: #090d16;
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.35);
--shadow-md: 0 8px 24px rgba(0, 0, 0, 0.45);
--shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.55);
--shadow-glow: 0 0 35px rgba(99, 102, 241, 0.2);
}
/* Light Theme */
[data-theme='light'] {
--bg-app: #f4f6fa;
--bg-surface: #ffffff;
--bg-surface-elevated: #f8fafc;
--bg-glass: rgba(255, 255, 255, 0.85);
--bg-glass-card: rgba(255, 255, 255, 0.9);
--bg-input: #ffffff;
--border-subtle: rgba(0, 0, 0, 0.09);
--border-glow: rgba(99, 102, 241, 0.3);
--border-focus: #6366f1;
--text-main: #0f172a;
--text-secondary: #475569;
--text-muted: #94a3b8;
--text-inverse: #ffffff;
--shadow-sm: 0 2px 6px rgba(0, 0, 0, 0.04);
--shadow-md: 0 8px 20px rgba(15, 23, 42, 0.08);
--shadow-lg: 0 20px 40px rgba(15, 23, 42, 0.12);
--shadow-glow: 0 0 30px rgba(99, 102, 241, 0.15);
}
+23
View File
@@ -0,0 +1,23 @@
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/token': {
target: 'http://localhost:9292',
changeOrigin: true,
secure: false,
},
'/api': {
target: 'http://localhost:9292',
changeOrigin: true,
secure: false,
},
},
},
})