442 lines
25 KiB
TypeScript
442 lines
25 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { DataService, setApiMode } from '../services/dataService';
|
|
import { UserDTO, SystemConfig } from '../types';
|
|
import { ShieldAlert, Database, Users, Terminal, LogOut, Save, RefreshCw, Power, Lock, Search } from 'lucide-react';
|
|
import Image from 'next/image';
|
|
import Head from 'next/head';
|
|
|
|
export default function AdminConsole() {
|
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
const [username, setUsername] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
|
|
// Dashboard State
|
|
const [activeTab, setActiveTab] = useState<'USERS' | 'DB' | 'LOGS'>('USERS');
|
|
const [users, setUsers] = useState<UserDTO[]>([]);
|
|
const [config, setConfig] = useState<SystemConfig | null>(null);
|
|
const [logs, setLogs] = useState<string[]>([]);
|
|
const [currentMode, setCurrentMode] = useState<string>('MOCK');
|
|
|
|
// Fake Terminal Logs
|
|
const logContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
// Client-side only init
|
|
setCurrentMode(DataService.getMode());
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
loadDashboardData();
|
|
const cleanup = startLogStream();
|
|
return cleanup;
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
useEffect(() => {
|
|
if (logContainerRef.current) {
|
|
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
|
|
}
|
|
}, [logs]);
|
|
|
|
const startLogStream = () => {
|
|
if (DataService.getMode() === 'REAL') {
|
|
setLogs(['[SYSTEM] Attempting to connect to live backend log stream...', '[WARN] Log streaming over WebSocket not fully implemented in Preview']);
|
|
return () => { };
|
|
}
|
|
|
|
const phrases = [
|
|
"Connection pool optimized...",
|
|
"User u_002 requested packet 0x4F...",
|
|
"Analysing traffic pattern...",
|
|
"Database heartbeat: STABLE",
|
|
"WARNING: High latency on node US-EAST-4",
|
|
"Backing up sector 7...",
|
|
"New material indexed successfully."
|
|
];
|
|
const interval = setInterval(() => {
|
|
const phrase = phrases[Math.floor(Math.random() * phrases.length)];
|
|
const time = new Date().toISOString().split('T')[1].replace('Z', '');
|
|
setLogs(prev => [...prev.slice(-20), `[${time}] ${phrase}`]);
|
|
}, 2000);
|
|
return () => clearInterval(interval);
|
|
};
|
|
|
|
const loadDashboardData = async () => {
|
|
try {
|
|
const u = await DataService.getAllUsers();
|
|
setUsers(u);
|
|
const c = await DataService.getSystemConfig();
|
|
setConfig(c);
|
|
} catch (e) {
|
|
setLogs(prev => [...prev, `[ERROR] Failed to fetch data: ${e}`]);
|
|
}
|
|
};
|
|
|
|
const handleLogin = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setError('');
|
|
const success = await DataService.verifyAdmin(username, password);
|
|
if (success) {
|
|
setIsAuthenticated(true);
|
|
setLogs(['[SYSTEM] Root access granted.', '[SYSTEM] Initializing admin daemon...']);
|
|
} else {
|
|
setError('ACCESS_DENIED // INVALID_CREDENTIALS');
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
const handleConfigSave = async () => {
|
|
if (!config) return;
|
|
setLoading(true);
|
|
try {
|
|
await DataService.updateSystemConfig(config);
|
|
setLogs(prev => [...prev, '[SYSTEM] Database configuration updated. Restarting services...']);
|
|
alert("SYSTEM CONFIG UPDATED.");
|
|
} catch (e) {
|
|
alert("Failed to update config.");
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
const handleToggleUser = async (uid: string) => {
|
|
const updated = await DataService.toggleUserStatus(uid);
|
|
if (updated) {
|
|
setUsers(users.map(u => u.id === uid ? updated : u));
|
|
setLogs(prev => [...prev, `[ADMIN] User ${updated.username} status changed to ${updated.status}`]);
|
|
}
|
|
};
|
|
|
|
const handleUpdateRole = async (uid: string, role: 'USER' | 'CREATOR' | 'MANAGER' | 'ADMIN') => {
|
|
try {
|
|
const updated = await DataService.updateUserRole(uid, role as any);
|
|
setUsers(prev => prev.map(u => u.id === uid ? updated : u));
|
|
setLogs(prev => [...prev, `[ADMIN] Role of ${updated.username} -> ${updated.role}`]);
|
|
} catch (e) {
|
|
setLogs(prev => [...prev, `[ERROR] Failed to update role: ${e}`]);
|
|
}
|
|
};
|
|
|
|
const handleModeSwitch = (mode: 'MOCK' | 'REAL') => {
|
|
if (mode === 'REAL' && !confirm("WARNING: Switching to REAL mode will attempt to connect to a running MySQL backend at localhost:3000. \n\nContinue?")) {
|
|
return;
|
|
}
|
|
setApiMode(mode);
|
|
setCurrentMode(mode);
|
|
};
|
|
|
|
if (!isAuthenticated) {
|
|
return (
|
|
<div className="min-h-screen bg-black text-red-600 font-mono flex items-center justify-center p-4 relative overflow-hidden">
|
|
<Head>
|
|
<title>NEXUS_MAT.OS</title>
|
|
</Head>
|
|
{/* Background Grid */}
|
|
<div className="absolute inset-0 z-0 opacity-20 pointer-events-none"
|
|
style={{ backgroundImage: 'linear-gradient(rgba(255, 0, 0, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 0, 0, 0.1) 1px, transparent 1px)', backgroundSize: '20px 20px' }}>
|
|
</div>
|
|
|
|
<div className="w-full max-w-md z-10 border border-red-900 bg-black/80 backdrop-blur-sm p-8 shadow-[0_0_50px_rgba(220,38,38,0.2)]">
|
|
<div className="flex justify-center mb-8">
|
|
<ShieldAlert size={64} className="animate-pulse" />
|
|
</div>
|
|
<h1 className="text-3xl font-bold text-center mb-2 tracking-widest">NEXUS<span className="text-white">_CORE</span></h1>
|
|
<p className="text-center text-xs text-red-800 mb-8">RESTRICTED AREA // AUTHORIZED PERSONNEL ONLY</p>
|
|
|
|
<form onSubmit={handleLogin} className="space-y-6">
|
|
<div className="space-y-2">
|
|
<label className="text-xs">IDENTIFIER</label>
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
value={username}
|
|
onChange={e => setUsername(e.target.value)}
|
|
className="w-full bg-red-950/20 border border-red-900 p-3 text-red-500 focus:outline-none focus:border-red-500 transition-colors"
|
|
placeholder="admin"
|
|
/>
|
|
<Lock className="absolute right-3 top-3 text-red-900" size={16} />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-xs">KEY_PHRASE</label>
|
|
<div className="relative">
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
onChange={e => setPassword(e.target.value)}
|
|
className="w-full bg-red-950/20 border border-red-900 p-3 text-red-500 focus:outline-none focus:border-red-500 transition-colors"
|
|
placeholder="••••••••"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="text-xs bg-red-950/50 border border-red-500 p-2 text-center animate-pulse">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
disabled={loading}
|
|
className="w-full py-4 bg-red-700 hover:bg-red-600 text-black font-bold tracking-widest transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{loading ? 'AUTHENTICATING...' : 'ESTABLISH_CONNECTION'}
|
|
</button>
|
|
</form>
|
|
|
|
<div className="mt-8 text-[10px] text-center text-red-900">
|
|
SYSTEM_ID: 0x8842-ALPHA <br /> IP_LOGGED: 127.0.0.1
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#050000] text-red-500 font-mono flex flex-col">
|
|
<Head>
|
|
<title>NEXUS_MAT.OS</title>
|
|
</Head>
|
|
{/* Top Bar */}
|
|
<header className="h-16 border-b border-red-900/50 bg-black/50 flex items-center justify-between px-6 backdrop-blur">
|
|
<div className="flex items-center gap-4">
|
|
<Terminal size={24} />
|
|
<span className="text-xl font-bold tracking-tighter text-white">CORE<span className="text-red-600">.CONSOLE</span></span>
|
|
<span className="px-2 py-0.5 text-[10px] border border-red-800 rounded bg-red-900/20 text-red-400">ADMIN_MODE</span>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<div className={`hidden md:flex items-center gap-2 text-xs ${currentMode === 'REAL' ? 'text-green-500' : 'text-yellow-500'}`}>
|
|
<span className="animate-pulse">●</span> {currentMode === 'REAL' ? 'LIVE_DATABASE' : 'SIMULATION_MODE'}
|
|
</div>
|
|
<button onClick={() => window.location.href = '/'} className="p-2 hover:bg-red-900/20 rounded border border-transparent hover:border-red-900 transition-colors" title="Exit to App">
|
|
<LogOut size={18} />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{/* Sidebar */}
|
|
<aside className="w-64 border-r border-red-900/30 bg-red-950/5 hidden md:flex flex-col">
|
|
<nav className="p-4 space-y-2">
|
|
<button
|
|
onClick={() => setActiveTab('USERS')}
|
|
className={`w-full flex items-center gap-3 p-3 text-sm transition-all ${activeTab === 'USERS' ? 'bg-red-900/20 border-l-2 border-red-500 text-white' : 'text-red-800 hover:text-red-400'}`}
|
|
>
|
|
<Users size={18} /> USER_REGISTRY
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('DB')}
|
|
className={`w-full flex items-center gap-3 p-3 text-sm transition-all ${activeTab === 'DB' ? 'bg-red-900/20 border-l-2 border-red-500 text-white' : 'text-red-800 hover:text-red-400'}`}
|
|
>
|
|
<Database size={18} /> DATABASE_CONFIG
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('LOGS')}
|
|
className={`w-full flex items-center gap-3 p-3 text-sm transition-all ${activeTab === 'LOGS' ? 'bg-red-900/20 border-l-2 border-red-500 text-white' : 'text-red-800 hover:text-red-400'}`}
|
|
>
|
|
<Terminal size={18} /> SYSTEM_LOGS
|
|
</button>
|
|
</nav>
|
|
<div className="mt-auto p-4 border-t border-red-900/30">
|
|
<div className="text-[10px] text-red-900">
|
|
SERVER_UPTIME: 489h 22m<br />
|
|
MEMORY: 64TB / 128TB
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main Content */}
|
|
<main className="flex-1 overflow-y-auto p-8 relative">
|
|
{/* Background Decoration */}
|
|
<div className="absolute top-0 right-0 p-10 opacity-10 pointer-events-none">
|
|
<Database size={200} />
|
|
</div>
|
|
|
|
{activeTab === 'USERS' && (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-end border-b border-red-900/30 pb-4">
|
|
<h2 className="text-2xl font-bold text-white">USER_DATABASE</h2>
|
|
<div className="flex gap-2">
|
|
<input placeholder="SEARCH_ID..." className="bg-black border border-red-900 p-1 text-xs text-red-500 w-40 focus:outline-none" />
|
|
<button className="p-1 bg-red-900/20 border border-red-900"><Search size={14} /></button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left text-sm">
|
|
<thead className="text-xs text-red-700 uppercase tracking-wider bg-red-950/10">
|
|
<tr>
|
|
<th className="p-3">AVATAR</th>
|
|
<th className="p-3">IDENTIFIER</th>
|
|
<th className="p-3">ROLE</th>
|
|
<th className="p-3">STATUS</th>
|
|
<th className="p-3">CREATED</th>
|
|
<th className="p-3 text-right">ACTIONS</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-red-900/20">
|
|
{users.map(user => (
|
|
<tr key={user.id} className="hover:bg-red-900/5 transition-colors group">
|
|
<td className="p-3">
|
|
<Image src={user.avatarUrl} alt="avatar" width={32} height={32} className="w-8 h-8 rounded-sm grayscale group-hover:grayscale-0 transition-all border border-red-900/50" />
|
|
</td>
|
|
<td className="p-3 font-bold text-gray-300">{user.username}<br /><span className="text-[10px] text-red-800">{user.id}</span></td>
|
|
<td className="p-3 text-xs">
|
|
<select
|
|
value={user.role}
|
|
onChange={e => handleUpdateRole(user.id, e.target.value as any)}
|
|
className="bg-black border border-red-900 text-red-500 text-xs p-1"
|
|
>
|
|
<option value="USER">USER</option>
|
|
<option value="CREATOR">CREATOR</option>
|
|
<option value="MANAGER">MANAGER</option>
|
|
<option value="ADMIN">ADMIN</option>
|
|
</select>
|
|
</td>
|
|
<td className="p-3">
|
|
<span className={`px-2 py-0.5 text-[10px] border ${user.status === 'ACTIVE' ? 'border-green-900 text-green-500' : 'border-red-500 text-red-500 animate-pulse'}`}>
|
|
{user.status}
|
|
</span>
|
|
</td>
|
|
<td className="p-3 text-xs text-red-800">{new Date(user.createdAt).toLocaleDateString()}</td>
|
|
<td className="p-3 text-right">
|
|
<button
|
|
onClick={() => handleToggleUser(user.id)}
|
|
className="px-3 py-1 border border-red-900 hover:bg-red-500 hover:text-black transition-colors text-xs"
|
|
>
|
|
{user.status === 'ACTIVE' ? 'BAN_USER' : 'RESTORE'}
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'DB' && config && (
|
|
<div className="max-w-2xl space-y-8">
|
|
<h2 className="text-2xl font-bold text-white border-b border-red-900/30 pb-4 flex items-center justify-between">
|
|
MYSQL_CONFIGURATION
|
|
|
|
<div className="flex items-center gap-4">
|
|
<span className="text-xs font-normal text-gray-400">DATA_SOURCE_MODE:</span>
|
|
<div className="flex border border-red-900 rounded overflow-hidden">
|
|
<button
|
|
onClick={() => handleModeSwitch('MOCK')}
|
|
className={`px-3 py-1 text-xs transition-colors ${currentMode === 'MOCK' ? 'bg-red-600 text-black font-bold' : 'hover:bg-red-900/20 text-red-500'}`}
|
|
>
|
|
SIMULATION
|
|
</button>
|
|
<button
|
|
onClick={() => handleModeSwitch('REAL')}
|
|
className={`px-3 py-1 text-xs transition-colors ${currentMode === 'REAL' ? 'bg-red-600 text-black font-bold' : 'hover:bg-red-900/20 text-red-500'}`}
|
|
>
|
|
LIVE_MYSQL
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</h2>
|
|
|
|
<div className={`grid grid-cols-1 md:grid-cols-2 gap-6 ${currentMode === 'MOCK' ? 'opacity-50 pointer-events-none grayscale' : ''}`}>
|
|
<div className="space-y-2">
|
|
<label className="text-xs text-red-700">HOST_ADDRESS</label>
|
|
<input
|
|
value={config.dbHost}
|
|
onChange={e => setConfig({ ...config, dbHost: e.target.value })}
|
|
className="w-full bg-black border border-red-900 p-3 text-white focus:border-red-500 outline-none"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-xs text-red-700">PORT</label>
|
|
<input
|
|
value={config.dbPort}
|
|
onChange={e => setConfig({ ...config, dbPort: e.target.value })}
|
|
className="w-full bg-black border border-red-900 p-3 text-white focus:border-red-500 outline-none"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-xs text-red-700">ROOT_USER</label>
|
|
<input
|
|
value={config.dbUser}
|
|
onChange={e => setConfig({ ...config, dbUser: e.target.value })}
|
|
className="w-full bg-black border border-red-900 p-3 text-white focus:border-red-500 outline-none"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-xs text-red-700">ENCRYPTED_PASS</label>
|
|
<input
|
|
type="password"
|
|
value={config.dbPass}
|
|
onChange={e => setConfig({ ...config, dbPass: e.target.value })}
|
|
className="w-full bg-black border border-red-900 p-3 text-white focus:border-red-500 outline-none"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-xs text-red-700">DATABASE_NAME</label>
|
|
<input
|
|
value={config.dbName}
|
|
onChange={e => setConfig({ ...config, dbName: e.target.value })}
|
|
className="w-full bg-black border border-red-900 p-3 text-white focus:border-red-500 outline-none"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-xs text-red-700">MAX_UPLOAD_MB</label>
|
|
<input
|
|
value={config.maxUploadMB}
|
|
onChange={e => setConfig({ ...config, maxUploadMB: Number(e.target.value) || 0 })}
|
|
className="w-full bg-black border border-red-900 p-3 text-white focus:border-red-500 outline-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{currentMode === 'MOCK' && (
|
|
<div className="p-4 border border-yellow-800 bg-yellow-900/10 text-yellow-600 text-xs font-mono">
|
|
[INFO] Currently running in SIMULATION MODE.
|
|
<br />Configuration editing is disabled.
|
|
<br />Switch to LIVE_MYSQL to configure connection string.
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center gap-4 pt-6 border-t border-red-900/30">
|
|
<button
|
|
onClick={handleConfigSave}
|
|
disabled={loading || currentMode === 'MOCK'}
|
|
className="flex items-center gap-2 px-6 py-3 bg-red-600 text-black font-bold hover:bg-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{loading ? <RefreshCw className="animate-spin" size={18} /> : <Save size={18} />}
|
|
COMMIT_CHANGES
|
|
</button>
|
|
<button className="px-6 py-3 border border-red-900 text-red-500 hover:bg-red-900/20 transition-colors flex items-center gap-2">
|
|
<Power size={18} /> SHUTDOWN_DB
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'LOGS' && (
|
|
<div className="h-full flex flex-col">
|
|
<h2 className="text-2xl font-bold text-white border-b border-red-900/30 pb-4 mb-4">KERNEL_LOGS</h2>
|
|
<div
|
|
ref={logContainerRef}
|
|
className="flex-1 bg-black/50 border border-red-900/50 p-4 font-mono text-xs overflow-y-auto font-light"
|
|
>
|
|
{logs.map((log, i) => (
|
|
<div key={i} className="mb-1 text-red-400 hover:text-white transition-colors cursor-default">
|
|
<span className="opacity-50 mr-2">{i.toString().padStart(4, '0')}</span>
|
|
{log}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|