feat: frontend support for mandatory password change and react-router integration
This commit is contained in:
@@ -1,7 +1,11 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import Login from './components/Login';
|
import Login from './components/Login';
|
||||||
import Layout from './components/Layout';
|
import Layout from './components/Layout';
|
||||||
import Dashboard from './components/Dashboard';
|
import Dashboard from './components/Dashboard';
|
||||||
|
import LiveLogs from './components/LiveLogs';
|
||||||
|
import Attackers from './components/Attackers';
|
||||||
|
import Config from './components/Config';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
|
const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
|
||||||
@@ -32,9 +36,17 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Router>
|
||||||
<Layout onLogout={handleLogout} onSearch={handleSearch}>
|
<Layout onLogout={handleLogout} onSearch={handleSearch}>
|
||||||
<Dashboard searchQuery={searchQuery} />
|
<Routes>
|
||||||
|
<Route path="/" element={<Dashboard searchQuery={searchQuery} />} />
|
||||||
|
<Route path="/live-logs" element={<LiveLogs />} />
|
||||||
|
<Route path="/attackers" element={<Attackers />} />
|
||||||
|
<Route path="/config" element={<Config />} />
|
||||||
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
decnet_web/src/components/Attackers.tsx
Normal file
20
decnet_web/src/components/Attackers.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Activity } from 'lucide-react';
|
||||||
|
import './Dashboard.css';
|
||||||
|
|
||||||
|
const Attackers: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="logs-section">
|
||||||
|
<div className="section-header">
|
||||||
|
<Activity size={20} />
|
||||||
|
<h2>ATTACKER PROFILES</h2>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: '40px', textAlign: 'center', opacity: 0.5 }}>
|
||||||
|
<p>NO ACTIVE THREATS PROFILED YET.</p>
|
||||||
|
<p style={{ marginTop: '10px', fontSize: '0.8rem' }}>(Attackers view placeholder)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Attackers;
|
||||||
20
decnet_web/src/components/Config.tsx
Normal file
20
decnet_web/src/components/Config.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Settings } from 'lucide-react';
|
||||||
|
import './Dashboard.css';
|
||||||
|
|
||||||
|
const Config: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="logs-section">
|
||||||
|
<div className="section-header">
|
||||||
|
<Settings size={20} />
|
||||||
|
<h2>SYSTEM CONFIGURATION</h2>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: '40px', textAlign: 'center', opacity: 0.5 }}>
|
||||||
|
<p>CONFIGURATION READ-ONLY MODE ACTIVE.</p>
|
||||||
|
<p style={{ marginTop: '10px', fontSize: '0.8rem' }}>(Config view placeholder)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Config;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
import { Menu, X, Search, Activity, LayoutDashboard, Terminal, Settings, LogOut } from 'lucide-react';
|
import { Menu, X, Search, Activity, LayoutDashboard, Terminal, Settings, LogOut } from 'lucide-react';
|
||||||
import './Layout.css';
|
import './Layout.css';
|
||||||
|
|
||||||
@@ -30,10 +31,10 @@ const Layout: React.FC<LayoutProps> = ({ children, onLogout, onSearch }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="sidebar-nav">
|
<nav className="sidebar-nav">
|
||||||
<NavItem icon={<LayoutDashboard size={20} />} label="Dashboard" active open={sidebarOpen} />
|
<NavItem to="/" icon={<LayoutDashboard size={20} />} label="Dashboard" open={sidebarOpen} />
|
||||||
<NavItem icon={<Terminal size={20} />} label="Live Logs" open={sidebarOpen} />
|
<NavItem to="/live-logs" icon={<Terminal size={20} />} label="Live Logs" open={sidebarOpen} />
|
||||||
<NavItem icon={<Activity size={20} />} label="Attackers" open={sidebarOpen} />
|
<NavItem to="/attackers" icon={<Activity size={20} />} label="Attackers" open={sidebarOpen} />
|
||||||
<NavItem icon={<Settings size={20} />} label="Config" open={sidebarOpen} />
|
<NavItem to="/config" icon={<Settings size={20} />} label="Config" open={sidebarOpen} />
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="sidebar-footer">
|
<div className="sidebar-footer">
|
||||||
@@ -72,17 +73,17 @@ const Layout: React.FC<LayoutProps> = ({ children, onLogout, onSearch }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface NavItemProps {
|
interface NavItemProps {
|
||||||
|
to: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
active?: boolean;
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavItem: React.FC<NavItemProps> = ({ icon, label, active, open }) => (
|
const NavItem: React.FC<NavItemProps> = ({ to, icon, label, open }) => (
|
||||||
<div className={`nav-item ${active ? 'active' : ''}`}>
|
<NavLink to={to} className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`} end={to === '/'}>
|
||||||
{icon}
|
{icon}
|
||||||
{open && <span className="nav-label">{label}</span>}
|
{open && <span className="nav-label">{label}</span>}
|
||||||
</div>
|
</NavLink>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Layout;
|
export default Layout;
|
||||||
|
|||||||
20
decnet_web/src/components/LiveLogs.tsx
Normal file
20
decnet_web/src/components/LiveLogs.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Terminal } from 'lucide-react';
|
||||||
|
import './Dashboard.css';
|
||||||
|
|
||||||
|
const LiveLogs: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="logs-section">
|
||||||
|
<div className="section-header">
|
||||||
|
<Terminal size={20} />
|
||||||
|
<h2>FULL LIVE LOG STREAM</h2>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: '40px', textAlign: 'center', opacity: 0.5 }}>
|
||||||
|
<p>STREAM ESTABLISHED. WAITING FOR INCOMING DATA...</p>
|
||||||
|
<p style={{ marginTop: '10px', fontSize: '0.8rem' }}>(Dedicated Live Logs view placeholder)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LiveLogs;
|
||||||
@@ -12,19 +12,58 @@ const Login: React.FC<LoginProps> = ({ onLogin }) => {
|
|||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [needsPasswordChange, setNeedsPasswordChange] = useState(false);
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [tempToken, setTempToken] = useState('');
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleLoginSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.post('/auth/login', { username, password });
|
const response = await api.post('/auth/login', { username, password });
|
||||||
|
const { access_token, must_change_password } = response.data;
|
||||||
|
|
||||||
|
if (must_change_password) {
|
||||||
|
setTempToken(access_token);
|
||||||
|
setNeedsPasswordChange(true);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem('token', access_token);
|
||||||
|
onLogin(access_token);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.response?.data?.detail || 'Authentication failed');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangePasswordSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
setError('Passwords do not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post('/auth/change-password',
|
||||||
|
{ old_password: password, new_password: newPassword },
|
||||||
|
{ headers: { Authorization: `Bearer ${tempToken}` } }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Re-authenticate to get a fresh token with must_change_password=false
|
||||||
|
const response = await api.post('/auth/login', { username, password: newPassword });
|
||||||
const { access_token } = response.data;
|
const { access_token } = response.data;
|
||||||
|
|
||||||
localStorage.setItem('token', access_token);
|
localStorage.setItem('token', access_token);
|
||||||
onLogin(access_token);
|
onLogin(access_token);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.detail || 'Authentication failed');
|
setError(err.response?.data?.detail || 'Password change failed');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -39,7 +78,8 @@ const Login: React.FC<LoginProps> = ({ onLogin }) => {
|
|||||||
<p>AUTHORIZED PERSONNEL ONLY</p>
|
<p>AUTHORIZED PERSONNEL ONLY</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="login-form">
|
{!needsPasswordChange ? (
|
||||||
|
<form onSubmit={handleLoginSubmit} className="login-form">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>IDENTIFIER</label>
|
<label>IDENTIFIER</label>
|
||||||
<input
|
<input
|
||||||
@@ -66,6 +106,42 @@ const Login: React.FC<LoginProps> = ({ onLogin }) => {
|
|||||||
{loading ? 'VERIFYING...' : 'ESTABLISH CONNECTION'}
|
{loading ? 'VERIFYING...' : 'ESTABLISH CONNECTION'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleChangePasswordSubmit} className="login-form">
|
||||||
|
<div className="form-group" style={{ textAlign: 'center', marginBottom: '10px' }}>
|
||||||
|
<p className="violet-accent">MANDATORY SECURITY UPDATE</p>
|
||||||
|
<p style={{ fontSize: '0.8rem', opacity: 0.7 }}>Please establish a new access key</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>NEW ACCESS KEY</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
minLength={8}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>CONFIRM KEY</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
minLength={8}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error-msg">{error}</div>}
|
||||||
|
|
||||||
|
<button type="submit" disabled={loading}>
|
||||||
|
{loading ? 'UPDATING...' : 'UPDATE SECURE KEY'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="login-footer">
|
<div className="login-footer">
|
||||||
<span>SECURE PROTOCOL v1.0</span>
|
<span>SECURE PROTOCOL v1.0</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user