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 { 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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 { 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;
|
||||
|
||||
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 [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>
|
||||
|
||||
Reference in New Issue
Block a user