feat: frontend support for mandatory password change and react-router integration

This commit is contained in:
2026-04-07 15:16:11 -04:00
parent 52c26a2891
commit 05e71f6d2e
6 changed files with 187 additions and 38 deletions

View File

@@ -1,7 +1,11 @@
import { useState, useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import Login from './components/Login';
import Layout from './components/Layout';
import Dashboard from './components/Dashboard';
import LiveLogs from './components/LiveLogs';
import Attackers from './components/Attackers';
import Config from './components/Config';
function App() {
const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
@@ -32,9 +36,17 @@ function App() {
}
return (
<Router>
<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>
</Router>
);
}

View 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;

View 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;

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { NavLink } from 'react-router-dom';
import { Menu, X, Search, Activity, LayoutDashboard, Terminal, Settings, LogOut } from 'lucide-react';
import './Layout.css';
@@ -30,10 +31,10 @@ const Layout: React.FC<LayoutProps> = ({ children, onLogout, onSearch }) => {
</div>
<nav className="sidebar-nav">
<NavItem icon={<LayoutDashboard size={20} />} label="Dashboard" active open={sidebarOpen} />
<NavItem icon={<Terminal size={20} />} label="Live Logs" open={sidebarOpen} />
<NavItem icon={<Activity size={20} />} label="Attackers" open={sidebarOpen} />
<NavItem icon={<Settings size={20} />} label="Config" open={sidebarOpen} />
<NavItem to="/" icon={<LayoutDashboard size={20} />} label="Dashboard" open={sidebarOpen} />
<NavItem to="/live-logs" icon={<Terminal size={20} />} label="Live Logs" open={sidebarOpen} />
<NavItem to="/attackers" icon={<Activity size={20} />} label="Attackers" open={sidebarOpen} />
<NavItem to="/config" icon={<Settings size={20} />} label="Config" open={sidebarOpen} />
</nav>
<div className="sidebar-footer">
@@ -72,17 +73,17 @@ const Layout: React.FC<LayoutProps> = ({ children, onLogout, onSearch }) => {
};
interface NavItemProps {
to: string;
icon: React.ReactNode;
label: string;
active?: boolean;
open: boolean;
}
const NavItem: React.FC<NavItemProps> = ({ icon, label, active, open }) => (
<div className={`nav-item ${active ? 'active' : ''}`}>
const NavItem: React.FC<NavItemProps> = ({ to, icon, label, open }) => (
<NavLink to={to} className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`} end={to === '/'}>
{icon}
{open && <span className="nav-label">{label}</span>}
</div>
</NavLink>
);
export default Layout;

View 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;

View File

@@ -12,19 +12,58 @@ const Login: React.FC<LoginProps> = ({ onLogin }) => {
const [password, setPassword] = useState('');
const [error, setError] = useState('');
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();
setLoading(true);
setError('');
try {
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;
localStorage.setItem('token', access_token);
onLogin(access_token);
} catch (err: any) {
setError(err.response?.data?.detail || 'Authentication failed');
setError(err.response?.data?.detail || 'Password change failed');
} finally {
setLoading(false);
}
@@ -39,7 +78,8 @@ const Login: React.FC<LoginProps> = ({ onLogin }) => {
<p>AUTHORIZED PERSONNEL ONLY</p>
</div>
<form onSubmit={handleSubmit} className="login-form">
{!needsPasswordChange ? (
<form onSubmit={handleLoginSubmit} className="login-form">
<div className="form-group">
<label>IDENTIFIER</label>
<input
@@ -66,6 +106,42 @@ const Login: React.FC<LoginProps> = ({ onLogin }) => {
{loading ? 'VERIFYING...' : 'ESTABLISH CONNECTION'}
</button>
</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">
<span>SECURE PROTOCOL v1.0</span>