Files
hipctf/App.tsx
m0rph3us1987 1c756af238 initial commit
2026-01-07 13:27:11 +01:00

1250 lines
67 KiB
TypeScript

import React, { useState, useEffect, createContext, useContext, useMemo, useCallback } from 'react';
import { HashRouter, Routes, Route, Link, useNavigate, Navigate, useLocation } from 'react-router-dom';
import { LogIn, UserPlus, Trophy, Flag, Shield, LogOut, Settings, Clock, Plus, Trash2, Edit3, Download, ExternalLink, Menu, X, CheckCircle2, ChevronRight, Layers, Database, RefreshCw, Terminal, Crosshair, Radar, Zap, Box, User, Users, ShieldAlert, ShieldCheck, Heart, Sparkles, Coffee, Table, Paperclip, Newspaper, Image as ImageIcon, Type, Layout, Wand2, Monitor, Globe } from 'lucide-react';
import { Challenge, Team, Solve, CTFState, Difficulty, ChallengeFile, BlogPost } from './types';
import { CATEGORIES, DIFFICULTIES } from './constants';
import { calculateChallengeValue, calculateTeamTotalScore, getFirstBloodBonus } from './services/scoring';
import { api } from './services/api';
// --- Context & State Management ---
interface CTFContextType {
state: CTFState;
currentUser: Team | null;
login: (name: string, pass: string) => Promise<boolean>;
register: (name: string, pass: string) => Promise<void>;
logout: () => void;
submitFlag: (challengeId: string, flag: string) => Promise<boolean>;
toggleCtf: () => Promise<void>;
upsertChallenge: (data: FormData, id?: string) => Promise<void>;
deleteChallenge: (id: string) => Promise<void>;
updateTeam: (id: string, name: string, isDisabled: boolean, isAdmin: boolean, password?: string) => Promise<void>;
updateProfile: (password?: string) => Promise<void>;
deleteTeam: (id: string) => Promise<void>;
createBlogPost: (data: { title: string, content: string }) => Promise<void>;
updateBlogPost: (id: string, data: { title: string, content: string }) => Promise<void>;
deleteBlogPost: (id: string) => Promise<void>;
updateConfig: (formData: FormData) => Promise<void>;
refreshState: () => Promise<void>;
}
const CTFContext = createContext<CTFContextType | null>(null);
const useCTF = () => {
const context = useContext(CTFContext);
if (!context) throw new Error('useCTF must be used within provider');
return context;
};
// --- Helper Components ---
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { currentUser } = useCTF();
const location = useLocation();
if (!currentUser) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
};
const CategoryIcon: React.FC<{ category: string; size?: number; color?: string }> = ({ category, size = 32, color = "currentColor" }) => {
switch (category) {
case 'WEB': return <Radar size={size} color={color} />;
case 'PWN': return <Zap size={size} color={color} />;
case 'REV': return <RefreshCw size={size} color={color} />;
case 'CRY': return <Box size={size} color={color} />;
default: return <Terminal size={size} color={color} />;
}
};
const getDifficultyColorClass = (difficulty: Difficulty) => {
switch (difficulty) {
case 'Low': return 'text-green-500';
case 'Medium': return 'text-yellow-500';
case 'High': return 'text-red-500';
default: return 'text-slate-400';
}
};
const Button: React.FC<React.ButtonHTMLAttributes<HTMLButtonElement> & { variant?: 'primary' | 'secondary' }> = ({ children, variant = 'primary', className = "", ...props }) => {
const styles = variant === 'primary'
? "bg-[#ff0000] text-black border-2 border-[#ff0000] hover:bg-black hover:text-[#ff0000] disabled:opacity-50"
: "bg-black text-[#bf00ff] border-2 border-[#bf00ff] hover:bg-[#bf00ff] hover:text-black disabled:opacity-50";
return (
<button className={`px-4 py-2 font-black tracking-tighter transition-all active:scale-95 ${styles} ${className}`} {...props}>
{children}
</button>
);
};
const ChallengeModal: React.FC<{
challenge: Challenge;
onClose: () => void;
}> = ({ challenge, onClose }) => {
const { state, currentUser, submitFlag, refreshState } = useCTF();
const [flagInput, setFlagInput] = useState('');
const [message, setMessage] = useState<{ text: string, type: 'success' | 'error' } | null>(null);
const handleFlagSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const result = await submitFlag(challenge.id, flagInput);
if (result) {
setMessage({ text: 'ACCESS GRANTED ✨', type: 'success' });
setFlagInput('');
setTimeout(() => { onClose(); setMessage(null); refreshState(); }, 1500);
} else {
setMessage({ text: 'RETRY SUGGESTED', type: 'error' });
}
} catch (err) {
setMessage({ text: 'COMMUNICATION ERROR', type: 'error' });
}
};
const connectionDetails = useMemo(() => {
const port = Number(challenge.port);
const ip = state.config.dockerIp;
const type = challenge.connectionType || 'nc';
if (port > 0) {
if (!ip) {
return currentUser?.isAdmin ? "ERROR: DOCKER_NODE_IP_NOT_CONFIGURED" : null;
}
return type === 'nc' ? `nc ${ip} ${port}` : `http://${ip}:${port}`;
}
return null;
}, [challenge.port, challenge.connectionType, state.config.dockerIp, currentUser?.isAdmin]);
return (
<div className="fixed inset-0 bg-black/95 z-[300] flex items-center justify-center p-4">
<div className="hxp-border border-4 max-w-2xl w-full bg-black p-8 relative overflow-y-auto max-h-[90vh] custom-scrollbar">
<button onClick={onClose} className="absolute top-4 right-4 bg-[#ff0000] text-black font-black p-2 hover:bg-white transition-colors"><X size={24} /></button>
<div className="mb-8">
<h3 className="text-4xl font-black italic tracking-tighter text-white mb-2">{challenge.title}</h3>
<div className="flex gap-4 font-bold text-xs tracking-widest text-slate-400">
<span className="text-[#bf00ff]">{challenge.category}</span> | <span className={getDifficultyColorClass(challenge.difficulty)}>{challenge.difficulty}</span> | <span className="text-white">{calculateChallengeValue(challenge.initialPoints, challenge.solves.length)} PTS</span>
</div>
</div>
<div className="hxp-border-purple p-6 mb-6 text-[#bf00ff] font-bold text-sm leading-relaxed whitespace-pre-wrap bg-[#bf00ff]/5">{challenge.description}</div>
{connectionDetails && (
<div className="mb-8 p-4 hxp-border border-[#00ccff] bg-[#00ccff]/5">
<h4 className="text-[10px] font-black text-[#00ccff] uppercase tracking-[0.2em] mb-2 flex items-center gap-2"><Globe size={12}/> Connect to:</h4>
<code className="block bg-black p-3 text-[#00ccff] font-black text-sm border border-[#00ccff]/30 break-all select-all cursor-pointer" title="Click to select">
{connectionDetails}
</code>
{connectionDetails.startsWith("ERROR") && (
<p className="text-[9px] text-[#ff0000] mt-2 font-black italic">ADMIN_NOTICE: Set Docker Node IP in General Config</p>
)}
</div>
)}
{challenge.files && challenge.files.length > 0 && (
<div className="mb-8 space-y-2">
<h4 className="text-[10px] font-black text-slate-500 uppercase tracking-widest">Available Files</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{challenge.files.map((file, idx) => (
<a key={idx} href={file.url} download={file.name} className="flex items-center gap-3 p-3 hxp-border-purple bg-white/5 hover:bg-white/10 transition-colors group">
<Download size={18} className="text-[#bf00ff]" />
<span className="text-xs font-black text-white truncate flex-1">{file.name}</span>
</a>
))}
</div>
</div>
)}
{currentUser ? (
!challenge.solves.includes(currentUser.id) ? (
<form onSubmit={handleFlagSubmit} className="space-y-4 mb-10">
<input type="text" placeholder="{flag: solution_here}" className="w-full bg-black hxp-border-purple p-4 outline-none focus:border-[#ff0000] text-white font-black" value={flagInput} onChange={(e) => setFlagInput(e.target.value)} autoFocus />
<div className="flex gap-4">
<Button type="submit" className="flex-1 text-xl py-4 font-black">Solve</Button>
{message && <div className={`flex-1 flex items-center justify-center font-black border-4 ${message.type === 'success' ? 'text-[#00ff00] border-[#00ff00]' : 'text-red-500 border-red-500'}`}>{message.text}</div>}
</div>
</form>
) : (
<div className="p-6 hxp-border border-[#00ff00] bg-[#00ff00]/10 text-[#00ff00] text-center font-black italic text-2xl tracking-widest mb-10 animate-pulse">CHALLENGE SOLVED </div>
)
) : (
<p className="text-center font-bold text-red-500 mb-10 uppercase tracking-widest">PLEASE SIGN IN TO CONTRIBUTE</p>
)}
<div className="border-t-4 border-[#333] pt-6">
<h4 className="text-sm font-black text-white tracking-[0.2em] mb-4 flex items-center gap-2 uppercase"><Sparkles size={14} /> SOLVER_LOG</h4>
<div className="space-y-1">
{challenge.solves.length > 0 ? (
state.solves.filter(s => s.challengeId === challenge.id).sort((a, b) => a.timestamp - b.timestamp).map((solve, idx) => {
const team = state.teams.find(t => t.id === solve.teamId);
const bonus = getFirstBloodBonus(idx);
return (
<div key={solve.teamId + idx} className="flex justify-between items-center p-3 bg-white/5 border border-white/10 text-[10px] font-bold">
<div className="flex items-center gap-3">
<span className={`w-5 h-5 flex items-center justify-center font-black ${idx === 0 ? 'bg-[#ff0000] text-black' : 'text-slate-500'}`}>{idx + 1}</span>
<span className="text-white">{team?.name}</span>
{bonus > 0 && <span className="text-[#00ff00] border border-[#00ff00]/30 px-2 py-0.5 uppercase tracking-tighter">PWNY POWER 🦄 +{Math.round(bonus*100)}%</span>}
</div>
<span className="text-slate-500 font-mono">{new Date(solve.timestamp).toLocaleTimeString()}</span>
</div>
);
})
) : (
<div className="text-center py-4 text-slate-700 font-black italic text-xs uppercase">No solutions discovered yet.</div>
)}
</div>
</div>
</div>
</div>
);
};
const ProfileSettingsModal: React.FC<{ onClose: () => void }> = ({ onClose }) => {
const { updateProfile } = useCTF();
const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (password !== confirm) return setError('AUTH_MISMATCH: PASSWORDS_DO_NOT_MATCH');
try {
await updateProfile(password);
setSuccess(true);
setTimeout(onClose, 2000);
} catch (err: any) {
setError(err.message || 'UPDATE_FAILED');
}
};
return (
<div className="fixed inset-0 bg-black/95 z-[500] flex items-center justify-center p-4">
<div className="hxp-border border-4 max-w-md w-full bg-black p-8 relative">
<button onClick={onClose} className="absolute top-4 right-4 text-white hover:text-[#ff0000]"><X size={24}/></button>
<h3 className="text-3xl font-black italic text-white mb-8 uppercase">IDENTITY_SETTINGS</h3>
{success ? (
<div className="p-8 hxp-border border-[#00ff00] text-[#00ff00] font-black text-center animate-pulse">
CREDENTIALS_UPDATED_SUCCESSFULLY
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-[10px] font-black text-slate-500 uppercase mb-2">NEW_ACCESS_KEY</label>
<input type="password" placeholder="••••••••" className="w-full bg-black hxp-border-purple p-4 outline-none focus:border-[#ff0000] text-white font-black" value={password} onChange={e => setPassword(e.target.value)} required />
</div>
<div>
<label className="block text-[10px] font-black text-slate-500 uppercase mb-2">CONFIRM_KEY</label>
<input type="password" placeholder="••••••••" className="w-full bg-black hxp-border-purple p-4 outline-none focus:border-[#ff0000] text-white font-black" value={confirm} onChange={e => setConfirm(e.target.value)} required />
</div>
{error && <p className="text-red-500 font-black text-xs italic">{error}</p>}
<Button type="submit" className="w-full py-4 text-xl">Confirm Update</Button>
</form>
)}
</div>
</div>
);
};
// --- Pages ---
const Blog: React.FC = () => {
const { state } = useCTF();
return (
<div className="max-w-4xl mx-auto py-20 px-6">
<div className="mb-12 border-b-4 border-[#ff0000] pb-6">
<h2 className="text-6xl font-black italic tracking-tighter text-white uppercase leading-none flex items-center gap-4">
<Newspaper size={48} className="text-[#ff0000]" /> BLOG_FEED
</h2>
<p className="text-[#bf00ff] font-bold text-xs tracking-widest mt-2 uppercase">Protocol: Global Broadcasts & Updates</p>
</div>
<div className="space-y-12">
{state.blogs.length > 0 ? (
state.blogs.map((post) => (
<div key={post.id} className="hxp-border-purple bg-white/5 p-8 group hover:bg-white/10 transition-colors relative">
<div className="absolute top-0 right-0 bg-[#bf00ff] text-black text-[10px] font-black px-4 py-1 uppercase tracking-widest">
{new Date(post.timestamp).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}
</div>
<h3 className="text-3xl font-black text-white italic mb-4 tracking-tighter group-hover:text-[#bf00ff] transition-colors">
{post.title}
</h3>
<div className="text-slate-400 font-bold text-sm leading-relaxed whitespace-pre-wrap border-l-4 border-[#333] pl-6 py-2">
{post.content}
</div>
</div>
))
) : (
<div className="py-20 text-center hxp-border border-dashed border-[#333] opacity-50">
<p className="text-slate-500 font-black italic uppercase text-xl">Awaiting broadcasts from headquarters...</p>
</div>
)}
</div>
</div>
);
};
const Home: React.FC = () => {
const { currentUser, state } = useCTF();
return (
<div className="max-w-4xl mx-auto py-20 px-4 text-center">
<div className="mb-12 relative inline-block">
{/* Logo or fallback generic icon - Replaced text with logo as requested */}
<div className="relative z-10 flex flex-col items-center justify-center mb-10">
{state.config.logoUrl ? (
<img src={state.config.logoUrl} alt="Logo" className="max-h-60 mx-auto object-contain drop-shadow-[0_0_15px_rgba(255,0,0,0.3)]" />
) : (
<Terminal size={120} className="text-[#ff0000] mb-4 animate-pulse" />
)}
</div>
<h1 className="text-8xl font-black italic tracking-tighter text-white block leading-none mb-4">
{state.config.conferenceName}
</h1>
</div>
<div className="hxp-border p-8 mb-10 max-w-2xl mx-auto bg-black relative group overflow-hidden">
<div className="absolute top-0 left-0 w-1 h-full bg-[#ff0000] group-hover:w-full transition-all duration-500 opacity-10"></div>
<p className="text-xl font-bold uppercase tracking-tight relative z-10">{state.config.landingText}</p>
</div>
<div className="flex flex-wrap justify-center gap-6">
{!currentUser ? (
<>
<Link to="/register"><Button className="text-xl px-10">Join Us</Button></Link>
<Link to="/login"><Button variant="secondary" className="text-xl px-10">Sign In</Button></Link>
</>
) : (
<div className="flex gap-4">
<Link to="/challenges"><Button className="text-xl px-10">Challenges</Button></Link>
<Link to="/blog"><Button variant="secondary" className="text-xl px-10">Read Blog</Button></Link>
</div>
)}
</div>
</div>
);
};
const ChallengeList: React.FC = () => {
const { state, currentUser } = useCTF();
const [selectedChallenge, setSelectedChallenge] = useState<Challenge | null>(null);
const difficultyWeight: Record<Difficulty, number> = { 'Low': 1, 'Medium': 2, 'High': 3 };
// Admins can see challenges even if competition is not started/paused
if (!state.isStarted && !currentUser?.isAdmin) {
return (
<div className="max-w-4xl mx-auto py-32 px-4 text-center">
<div className="hxp-border border-4 p-12 inline-block">
<Clock className="w-20 h-20 text-[#bf00ff] mx-auto mb-6 animate-pulse" />
<h2 className="text-4xl font-black italic mb-4 uppercase">BOARD_PAUSED</h2>
<p className="text-slate-500 font-bold tracking-widest uppercase">Waiting for the opening ceremony...</p>
</div>
</div>
);
}
return (
<div className="w-full px-6 py-12 min-h-screen">
{!state.isStarted && currentUser?.isAdmin && (
<div className="max-w-7xl mx-auto mb-8 p-4 hxp-border border-[#ffaa00] bg-[#ffaa00]/10 text-[#ffaa00] font-black text-center italic uppercase tracking-widest flex items-center justify-center gap-3">
<ShieldAlert size={20}/> Competition is currently paused. Preview mode active for Admins.
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-8 max-w-[1600px] mx-auto">
{CATEGORIES.map(category => {
const categoryChallenges = (state.challenges || [])
.filter(c => c.category === category)
.sort((a, b) => difficultyWeight[a.difficulty] - difficultyWeight[b.difficulty]);
if (categoryChallenges.length === 0) return null;
return (
<div key={category} className="flex flex-col gap-6">
<div className="flex flex-col items-center gap-2 mb-4">
<div className="p-3 bg-white/5 hxp-border border-[#333] hover:border-[#bf00ff] transition-colors">
<CategoryIcon category={category} size={48} color={category === 'WEB' ? '#ff0000' : category === 'PWN' ? '#ffaa00' : category === 'REV' ? '#00ccff' : '#bf00ff'} />
</div>
<h3 className="text-3xl font-black italic tracking-tighter text-white uppercase">{category}</h3>
</div>
<div className="flex flex-col gap-4">
{categoryChallenges.map(c => {
const isSolved = currentUser && (c.solves || []).includes(currentUser.id);
const currentPoints = calculateChallengeValue(c.initialPoints, (c.solves || []).length);
return (
<div
key={c.id}
className={`hxp-card p-4 group cursor-pointer border-2 transition-all ${isSolved ? 'border-[#00ff00] bg-[#00ff00]/5' : 'border-[#333] hover:border-[#bf00ff]'}`}
onClick={() => setSelectedChallenge(c)}
>
<div className={`text-[10px] font-black text-center mb-1 ${getDifficultyColorClass(c.difficulty)} uppercase tracking-[0.2em]`}>
{c.difficulty}
</div>
<h4 className="font-black text-center mb-3 text-[#bf00ff] group-hover:text-white transition-colors tracking-tight uppercase truncate">{c.title}</h4>
<div className="flex justify-between items-center px-2">
<div className="flex items-center gap-1">
{isSolved && <CheckCircle2 size={12} className="text-[#00ff00]" />}
<span className={`text-xs font-black ${isSolved ? 'text-white' : 'text-slate-400'}`}>
{currentPoints}
</span>
</div>
<div className="flex items-center gap-1">
<Heart size={10} className="text-slate-500" />
<span className="text-[10px] font-bold text-slate-500">{(c.solves || []).length}</span>
</div>
</div>
</div>
);
})}
</div>
</div>
);
})}
</div>
{selectedChallenge && (
<ChallengeModal challenge={selectedChallenge} onClose={() => setSelectedChallenge(null)} />
)}
</div>
);
};
const Scoreboard: React.FC = () => {
const { state } = useCTF();
const rankings = useMemo(() => {
return (state.teams || [])
.filter(t => !t.isAdmin && !t.isDisabled)
.map(team => ({
...team,
score: calculateTeamTotalScore(team.id, state.challenges, state.solves),
solveCount: state.solves.filter(s => s.teamId === team.id).length
}))
.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
}, [state]);
return (
<div className="max-w-5xl mx-auto py-12 px-6">
<div className="mb-12 border-b-4 border-[#ff0000] pb-6">
<h2 className="text-6xl font-black italic tracking-tighter text-white uppercase leading-none">LEADERBOARD</h2>
<div className="flex justify-between items-end">
<p className="text-[#bf00ff] font-bold text-xs tracking-widest mt-2 uppercase">Protocol: Global Standings</p>
<Link to="/matrix" className="text-[10px] font-black text-[#ff0000] hover:underline flex items-center gap-1 uppercase tracking-widest"><Table size={12}/> View_Matrix</Link>
</div>
</div>
<div className="hxp-border border-2 overflow-hidden bg-black">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-[#333] text-[10px] font-black uppercase tracking-[0.2em]">
<th className="p-4 w-20">RANK</th>
<th className="p-4">TEAM_IDENTIFIER</th>
<th className="p-4 text-center">SOLVES</th>
<th className="p-4 text-right">TOTAL_POINTS</th>
</tr>
</thead>
<tbody className="divide-y divide-white/10">
{rankings.map((team, idx) => (
<tr key={team.id} className={`group hover:bg-white/5 transition-colors ${idx < 3 ? 'bg-white/[0.02]' : ''}`}>
<td className="p-4">
<div className={`w-8 h-8 flex items-center justify-center font-black italic text-lg ${
idx === 0 ? 'bg-[#ffaa00] text-black rotate-[-12deg]' :
idx === 1 ? 'bg-slate-400 text-black rotate-[6deg]' :
idx === 2 ? 'bg-[#cd7f32] text-black rotate-[-6deg]' : 'text-slate-500'
}`}>
{idx + 1}
</div>
</td>
<td className="p-4">
<div className="flex items-center gap-3">
<span className="text-xl font-black text-white italic group-hover:text-[#bf00ff] transition-colors">{team.name}</span>
{idx === 0 && <Sparkles size={16} className="text-[#ffaa00] animate-pulse" />}
</div>
</td>
<td className="p-4 text-center font-black text-[#bf00ff]">{team.solveCount}</td>
<td className="p-4 text-right font-black text-2xl italic text-white">{team.score}</td>
</tr>
))}
</tbody>
</table>
{rankings.length === 0 && (
<div className="py-20 text-center font-black italic text-slate-600 uppercase">No team activity detected yet.</div>
)}
</div>
</div>
);
};
const ScoreMatrix: React.FC = () => {
const { state } = useCTF();
const sortedTeams = useMemo(() => {
return state.teams
.filter(t => !t.isAdmin && !t.isDisabled)
.map(t => ({ ...t, score: calculateTeamTotalScore(t.id, state.challenges, state.solves) }))
.sort((a, b) => b.score - a.score);
}, [state]);
return (
<div className="w-full overflow-x-auto py-12 px-6 custom-scrollbar">
<div className="mb-12 border-b-4 border-[#00ff00] pb-6 max-w-6xl mx-auto">
<h2 className="text-6xl font-black italic tracking-tighter text-white uppercase leading-none">SCORE_MATRIX</h2>
<p className="text-[#bf00ff] font-bold text-xs tracking-widest mt-2 uppercase">Protocol: Operational Overview</p>
</div>
<div className="inline-block min-w-full bg-black hxp-border overflow-hidden">
<table className="border-collapse w-full">
<thead>
<tr>
<th className="sticky left-0 z-20 bg-black p-4 text-[10px] font-black uppercase text-slate-500 border-b-2 border-r-2 border-[#333] whitespace-nowrap min-w-[200px]">Team / Challenge</th>
{state.challenges.map(c => (
<th key={c.id} className="p-2 border-b-2 border-r-2 border-[#333] min-w-[60px] relative">
<div className="h-56 flex items-center justify-center">
<span className="block whitespace-nowrap origin-center -rotate-90 text-[11px] font-black tracking-widest text-slate-400 uppercase w-0">
{c.title}
</span>
</div>
</th>
))}
</tr>
</thead>
<tbody>
{sortedTeams.map(team => (
<tr key={team.id} className="hover:bg-white/5 group">
<td className="sticky left-0 z-10 bg-black p-3 font-black italic text-sm border-r-2 border-b-2 border-[#333] whitespace-nowrap group-hover:text-[#bf00ff]">
{team.name}
</td>
{state.challenges.map(c => {
const isSolved = c.solves.includes(team.id);
return (
<td key={c.id} className={`p-0 border-r-2 border-b-2 border-[#333] text-center`}>
<div className={`w-full h-10 flex items-center justify-center ${isSolved ? 'bg-[#00ff00]/40' : ''}`}>
{isSolved && <CheckCircle2 size={16} className="text-[#00ff00]" />}
</div>
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
const Login: React.FC = () => {
const [name, setName] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const { login } = useCTF();
const navigate = useNavigate();
const location = useLocation();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const success = await login(name, password);
if (success) {
const from = (location.state as any)?.from?.pathname || '/';
navigate(from, { replace: true });
} else {
setError('ACCESS_DENIED: INVALID_CREDENTIALS');
}
};
return (
<div className="min-h-[80vh] flex items-center justify-center px-6">
<div className="w-full max-w-md">
<div className="hxp-border border-4 p-10 bg-black relative">
<div className="absolute -top-4 -left-4 bg-[#ff0000] text-black font-black px-4 py-1 text-xs italic">AUTH_GATE_01</div>
<h2 className="text-5xl font-black italic text-white mb-8 tracking-tighter uppercase">SIGN_IN</h2>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-[10px] font-black text-slate-500 uppercase mb-2 tracking-[0.2em]">Team_Identifier</label>
<input type="text" placeholder="Ghost_Protocol" className="w-full bg-black hxp-border-purple p-4 outline-none focus:border-[#ff0000] text-white font-black" value={name} onChange={e => setName(e.target.value)} required />
</div>
<div>
<label className="block text-[10px] font-black text-slate-500 uppercase mb-2 tracking-[0.2em]">Access_Key</label>
<input type="password" placeholder="••••••••" className="w-full bg-black hxp-border-purple p-4 outline-none focus:border-[#ff0000] text-white font-black" value={password} onChange={e => setPassword(e.target.value)} required />
</div>
{error && <p className="text-red-500 font-black text-xs italic animate-pulse">{error}</p>}
<Button type="submit" className="w-full py-4 text-xl">Initiate Session</Button>
</form>
<p className="mt-8 text-center text-[10px] font-bold text-slate-500 tracking-widest uppercase">
New operator? <Link to="/register" className="text-[#bf00ff] hover:underline">Register_Identity</Link>
</p>
</div>
</div>
</div>
);
};
const Register: React.FC = () => {
const [name, setName] = useState('');
const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState('');
const [userCaptcha, setUserCaptcha] = useState('');
const [captchaValue, setCaptchaValue] = useState('');
const [error, setError] = useState('');
const { register } = useCTF();
const navigate = useNavigate();
const generateCaptcha = useCallback(() => {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
let result = '';
for (let i = 0; i < 6; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
setCaptchaValue(result);
}, []);
useEffect(() => {
generateCaptcha();
}, [generateCaptcha]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (password !== confirm) return setError('AUTH_MISMATCH: PASSWORDS_DO_NOT_MATCH');
if (userCaptcha.toUpperCase() !== captchaValue) {
setError('AUTH_FAILURE: INVALID_CAPTCHA');
generateCaptcha();
setUserCaptcha('');
return;
}
try {
await register(name, password);
navigate('/challenges');
} catch (err: any) {
setError(err.message || 'REGISTRATION_FAILED');
generateCaptcha();
}
};
return (
<div className="min-h-[80vh] flex items-center justify-center px-6">
<div className="w-full max-w-md">
<div className="hxp-border border-4 p-10 bg-black relative">
<div className="absolute -top-4 -left-4 bg-[#bf00ff] text-black font-black px-4 py-1 text-xs italic">IDENTITY_GEN_01</div>
<h2 className="text-5xl font-black italic text-white mb-8 tracking-tighter uppercase">REGISTER</h2>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-[10px] font-black text-slate-500 uppercase mb-2 tracking-[0.2em]">Team_Name</label>
<input type="text" placeholder="Neon_Surge" className="w-full bg-black hxp-border-purple p-4 outline-none focus:border-[#ff0000] text-white font-black" value={name} onChange={e => setName(e.target.value)} required />
</div>
<div>
<label className="block text-[10px] font-black text-slate-500 uppercase mb-2 tracking-[0.2em]">Access_Key</label>
<input type="password" placeholder="••••••••" className="w-full bg-black hxp-border-purple p-4 outline-none focus:border-[#ff0000] text-white font-black" value={password} onChange={e => setPassword(e.target.value)} required />
</div>
<div>
<label className="block text-[10px] font-black text-slate-500 uppercase mb-2 tracking-[0.2em]">Confirm_Key</label>
<input type="password" placeholder="••••••••" className="w-full bg-black hxp-border-purple p-4 outline-none focus:border-[#ff0000] text-white font-black" value={confirm} onChange={e => setConfirm(e.target.value)} required />
</div>
<div className="space-y-4">
<label className="block text-[10px] font-black text-slate-500 uppercase tracking-widest">Human_Validation</label>
<div className="flex gap-4 items-center">
<div className="flex-1 hxp-border border-2 bg-white/5 p-4 flex items-center justify-center relative select-none">
<span className="text-2xl font-black italic tracking-[0.3em] text-[#ff0000] drop-shadow-[0_0_8px_rgba(255,0,0,0.5)]">
{captchaValue}
</span>
<button type="button" onClick={generateCaptcha} className="absolute right-2 text-slate-500 hover:text-white transition-colors"><RefreshCw size={16} /></button>
</div>
<input type="text" placeholder="TYPE" className="w-24 bg-black hxp-border-purple p-4 outline-none focus:border-[#ff0000] text-white font-black text-center" value={userCaptcha} onChange={e => setUserCaptcha(e.target.value)} required />
</div>
</div>
{error && <p className="text-red-500 font-black text-xs italic animate-pulse">{error}</p>}
<Button type="submit" className="w-full py-4 text-xl">Create Identity</Button>
</form>
</div>
</div>
</div>
);
};
const Admin: React.FC = () => {
const { state, toggleCtf, upsertChallenge, deleteChallenge, updateTeam, deleteTeam, createBlogPost, updateBlogPost, deleteBlogPost, updateConfig, refreshState } = useCTF();
const [editingChallenge, setEditingChallenge] = useState<Partial<Challenge> | null>(null);
const [newFiles, setNewFiles] = useState<File[]>([]);
const [currentFiles, setCurrentFiles] = useState<ChallengeFile[]>([]);
const [editingTeam, setEditingTeam] = useState<Partial<Team> & { newPassword?: string } | null>(null);
const [editingBlogPost, setEditingBlogPost] = useState<Partial<BlogPost> | null>(null);
// General Config State
const [confName, setConfName] = useState(state.config.conferenceName || '');
const [confLanding, setConfLanding] = useState(state.config.landingText || '');
const [confBgType, setConfBgType] = useState(state.config.bgType || 'color');
const [confBgColor, setConfBgColor] = useState(state.config.bgColor || '#000000');
const [confBgOpacity, setConfBgOpacity] = useState(state.config.bgOpacity || '0.5');
const [confBgBrightness, setConfBgBrightness] = useState(state.config.bgBrightness || '1.0');
const [confBgContrast, setConfBgContrast] = useState(state.config.bgContrast || '1.0');
const [confDockerIp, setConfDockerIp] = useState(state.config.dockerIp || '');
const [newLogo, setNewLogo] = useState<File | null>(null);
const [newBgImage, setNewBgImage] = useState<File | null>(null);
useEffect(() => {
setConfName(state.config.conferenceName || '');
setConfLanding(state.config.landingText || '');
setConfBgType(state.config.bgType || 'color');
setConfBgColor(state.config.bgColor || '#000000');
setConfBgOpacity(state.config.bgOpacity || '0.5');
setConfBgBrightness(state.config.bgBrightness || '1.0');
setConfBgContrast(state.config.bgContrast || '1.0');
setConfDockerIp(state.config.dockerIp || '');
}, [state.config]);
const handleConfigSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const fd = new FormData();
fd.append('conferenceName', confName);
fd.append('landingText', confLanding);
fd.append('bgType', confBgType);
fd.append('bgColor', confBgColor);
fd.append('bgOpacity', confBgOpacity);
fd.append('bgBrightness', confBgBrightness);
fd.append('bgContrast', confBgContrast);
fd.append('dockerIp', confDockerIp);
if (newLogo) fd.append('logo', newLogo);
if (newBgImage) fd.append('bgImage', newBgImage);
try {
await updateConfig(fd);
alert('CONFIGURATION_UPDATED');
} catch (err) {
alert('CONFIG_UPDATE_FAILED');
}
};
const handleChallengeSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!editingChallenge) return;
try {
const formData = new FormData();
Object.entries(editingChallenge).forEach(([key, value]) => {
if (key !== 'files' && key !== 'solves' && value !== undefined && value !== null) {
formData.append(key, value.toString());
}
});
formData.append('existingFiles', JSON.stringify(currentFiles));
newFiles.forEach(file => { formData.append('files', file); });
await upsertChallenge(formData, editingChallenge.id);
setEditingChallenge(null);
setNewFiles([]);
} catch (err: any) {
alert("Failed to update challenge.");
}
};
const handleTeamUpdate = async (e: React.FormEvent) => {
e.preventDefault();
if (!editingTeam || !editingTeam.id) return;
await updateTeam(editingTeam.id, editingTeam.name || '', !!editingTeam.isDisabled, !!editingTeam.isAdmin, editingTeam.newPassword);
setEditingTeam(null);
};
const handleBlogSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!editingBlogPost) return;
try {
if (editingBlogPost.id) {
await updateBlogPost(editingBlogPost.id, { title: editingBlogPost.title || '', content: editingBlogPost.content || '' });
} else {
await createBlogPost({ title: editingBlogPost.title || '', content: editingBlogPost.content || '' });
}
setEditingBlogPost(null);
} catch (err) {
alert('BLOG_SAVE_FAILED');
}
};
const removeExistingFile = (idx: number) => {
setCurrentFiles(prev => prev.filter((_, i) => i !== idx));
};
return (
<div className="max-w-6xl mx-auto py-12 px-6">
<div className="flex justify-between items-center mb-12 border-b-4 border-[#ff0000] pb-6">
<h2 className="text-5xl font-black italic tracking-tighter text-white uppercase leading-none">ADMIN_OVERRIDE</h2>
<Button onClick={toggleCtf} variant={state.isStarted ? 'secondary' : 'primary'} className="flex items-center gap-2">
{state.isStarted ? <Clock className="text-[#ff0000]" /> : <Zap className="text-black" />}
{state.isStarted ? 'PAUSE_BOARD' : 'RESUME_BOARD'}
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
<div className="space-y-12">
{/* General Settings */}
<section className="space-y-6">
<h3 className="text-2xl font-black italic text-[#ffaa00] uppercase flex items-center gap-2 border-b-2 border-[#ffaa00] pb-2">
<Layout size={20}/> GENERAL_CONFIG
</h3>
<form onSubmit={handleConfigSubmit} className="hxp-border border-2 border-[#ffaa00]/30 p-6 bg-[#ffaa00]/5 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-[10px] font-black text-slate-500 uppercase mb-2">Conference Name</label>
<input className="w-full bg-black hxp-border p-3 text-white font-black text-sm" value={confName} onChange={e => setConfName(e.target.value)} />
</div>
<div>
<label className="block text-[10px] font-black text-slate-500 uppercase mb-2">Docker Node IP</label>
<input placeholder="192.168.178.1" className="w-full bg-black hxp-border p-3 text-white font-black text-sm" value={confDockerIp} onChange={e => setConfDockerIp(e.target.value)} />
<p className="text-[9px] text-[#ffaa00] font-bold mt-1 uppercase">Stored in DB: {state.config.dockerIp || 'MISSING'}</p>
</div>
</div>
<div>
<label className="block text-[10px] font-black text-slate-500 uppercase mb-2">Landing Page Message</label>
<textarea className="w-full bg-black hxp-border p-3 text-white font-black text-sm h-24" value={confLanding} onChange={e => setConfLanding(e.target.value)} />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-[10px] font-black text-slate-500 uppercase mb-2">Logo</label>
<input type="file" className="text-xs" onChange={e => e.target.files && setNewLogo(e.target.files[0])} />
</div>
<div>
<label className="block text-[10px] font-black text-slate-500 uppercase mb-2">Background Type</label>
<select className="bg-black hxp-border p-2 w-full text-xs font-black uppercase" value={confBgType} onChange={e => setConfBgType(e.target.value)}>
<option value="color">Solid Color</option>
<option value="image">Background Image</option>
</select>
</div>
</div>
{confBgType === 'color' ? (
<div>
<label className="block text-[10px] font-black text-slate-500 uppercase mb-2">Background Color</label>
<input type="color" className="w-full h-10 bg-black border-2 border-slate-700" value={confBgColor} onChange={e => setConfBgColor(e.target.value)} />
</div>
) : (
<div className="space-y-6">
<div>
<label className="block text-[10px] font-black text-slate-500 uppercase mb-2">Background Image</label>
<input type="file" className="text-xs" onChange={e => e.target.files && setNewBgImage(e.target.files[0])} />
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<div className="flex justify-between items-center mb-1">
<label className="text-[10px] font-black text-slate-500 uppercase">Opacity</label>
<span className="text-[10px] font-bold text-white">{(parseFloat(confBgOpacity) * 100).toFixed(0)}%</span>
</div>
<input type="range" min="0" max="1" step="0.01" className="w-full h-2 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-[#ffaa00]" value={confBgOpacity} onChange={e => setConfBgOpacity(e.target.value)} />
</div>
<div>
<div className="flex justify-between items-center mb-1">
<label className="text-[10px] font-black text-slate-500 uppercase">Brightness</label>
<span className="text-[10px] font-bold text-white">{(parseFloat(confBgBrightness) * 100).toFixed(0)}%</span>
</div>
<input type="range" min="0" max="4" step="0.05" className="w-full h-2 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-[#00ccff]" value={confBgBrightness} onChange={e => setConfBgBrightness(e.target.value)} />
</div>
<div>
<div className="flex justify-between items-center mb-1">
<label className="text-[10px] font-black text-slate-500 uppercase">Contrast</label>
<span className="text-[10px] font-bold text-white">{(parseFloat(confBgContrast) * 100).toFixed(0)}%</span>
</div>
<input type="range" min="0" max="4" step="0.05" className="w-full h-2 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-[#bf00ff]" value={confBgContrast} onChange={e => setConfBgContrast(e.target.value)} />
</div>
</div>
</div>
)}
<Button type="submit" className="w-full py-3 bg-[#ffaa00] border-[#ffaa00]">Commit Settings</Button>
</form>
</section>
{/* Challenges */}
<section className="space-y-6">
<div className="flex justify-between items-center border-b-2 border-[#bf00ff] pb-2">
<h3 className="text-2xl font-black italic text-[#bf00ff] uppercase flex items-center gap-2"><Database size={20}/> CHALLENGES</h3>
<Button onClick={() => {
setEditingChallenge({ title: '', category: 'WEB', difficulty: 'Low', initialPoints: 100, flag: '', description: '', files: [], port: 0, connectionType: 'nc' });
setCurrentFiles([]);
setNewFiles([]);
}} className="flex items-center gap-2 text-xs">
<Plus size={14}/> NEW_CHAL
</Button>
</div>
<div className="space-y-4 max-h-[400px] overflow-y-auto custom-scrollbar pr-2">
{state.challenges.map(c => (
<div key={c.id} className="hxp-border-purple p-4 bg-white/5 flex justify-between items-center group">
<div>
<h4 className="font-black text-white uppercase">{c.title}</h4>
<p className="text-[10px] text-slate-500 font-mono uppercase">{c.category} | {c.difficulty} | {c.initialPoints} PTS {c.port ? `| PORT:${c.port}` : ''}</p>
</div>
<div className="flex gap-2">
<button onClick={() => {
setEditingChallenge(c);
setCurrentFiles(c.files || []);
setNewFiles([]);
}} className="p-2 text-[#bf00ff] hover:bg-[#bf00ff] hover:text-black transition-colors"><Edit3 size={16}/></button>
<button onClick={() => deleteChallenge(c.id)} className="p-2 text-[#ff0000] hover:bg-[#ff0000] hover:text-black transition-colors"><Trash2 size={16}/></button>
</div>
</div>
))}
</div>
</section>
</div>
<div className="space-y-12">
{/* Operators */}
<section className="space-y-6">
<h3 className="text-2xl font-black italic text-[#bf00ff] uppercase flex items-center gap-2 border-b-2 border-[#bf00ff] pb-2"><Users size={20}/> OPERATORS</h3>
<div className="hxp-border border-2 overflow-hidden">
<table className="w-full text-left">
<thead className="bg-[#333] text-[10px] font-black uppercase">
<tr>
<th className="p-3">NAME</th>
<th className="p-3">STATUS</th>
<th className="p-3 text-right">ACTIONS</th>
</tr>
</thead>
<tbody className="divide-y divide-white/10 text-xs font-bold">
{/* Show everyone except root admin */}
{state.teams.filter(t => t.id !== 'admin-0').map(team => (
<tr key={team.id} className="hover:bg-white/5">
<td className="p-3 text-white italic">{team.name}</td>
<td className="p-3">
<div className="flex flex-col gap-1">
<span className={team.isDisabled ? 'text-red-500' : 'text-[#00ff00]'}>
{team.isDisabled ? 'DISABLED' : 'ACTIVE'}
</span>
{team.isAdmin ? (
<span className="text-[#ffaa00] text-[8px] border border-[#ffaa00]/30 px-1 inline-block w-fit uppercase">ADMIN PRIVILEGES</span>
) : (
<span className="text-slate-500 text-[8px] border border-slate-500/30 px-1 inline-block w-fit uppercase">NORMAL OPERATOR</span>
)}
</div>
</td>
<td className="p-3 text-right">
<div className="flex justify-end gap-2">
{/* Admin Toggle */}
<button
onClick={() => updateTeam(team.id, team.name, !!team.isDisabled, !team.isAdmin)}
className={`p-1 transition-colors ${team.isAdmin ? 'text-[#ffaa00] hover:text-white' : 'text-slate-600 hover:text-[#ffaa00]'}`}
title={team.isAdmin ? "Demote from Admin" : "Promote to Admin"}
>
<Shield size={16} />
</button>
<button onClick={() => setEditingTeam(team)} className="p-1 hover:text-white transition-colors"><Edit3 size={16}/></button>
<button onClick={() => updateTeam(team.id, team.name, !team.isDisabled, !!team.isAdmin)} className="p-1 hover:text-white transition-colors" title={team.isDisabled ? "Enable Operator" : "Disable Operator"}>
{team.isDisabled ? <ShieldCheck size={16} /> : <ShieldAlert size={16} />}
</button>
<button onClick={() => deleteTeam(team.id)} className="p-1 text-[#ff0000] hover:text-white transition-colors" title="Delete Operator"><Trash2 size={16}/></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
{/* Blogs */}
<section className="space-y-6">
<div className="flex justify-between items-center border-b-2 border-[#ff0000] pb-2">
<h3 className="text-2xl font-black italic text-[#ff0000] uppercase flex items-center gap-2"><Newspaper size={20}/> BLOG_BROADCASTS</h3>
<Button onClick={() => setEditingBlogPost({ title: '', content: '' })} className="flex items-center gap-2 text-xs">
<Plus size={14}/> NEW_POST
</Button>
</div>
<div className="space-y-4 max-h-[400px] overflow-y-auto custom-scrollbar pr-2">
{state.blogs.map(post => (
<div key={post.id} className="hxp-border p-4 bg-white/5 flex justify-between items-center">
<div>
<h4 className="font-black text-white uppercase">{post.title}</h4>
<p className="text-[10px] text-slate-500 font-mono uppercase">{new Date(post.timestamp).toLocaleString()}</p>
</div>
<div className="flex gap-2">
<button onClick={() => setEditingBlogPost(post)} className="p-2 text-[#bf00ff] hover:bg-[#bf00ff] hover:text-black transition-colors"><Edit3 size={16}/></button>
<button onClick={() => deleteBlogPost(post.id)} className="p-2 text-[#ff0000] hover:bg-[#ff0000] hover:text-black transition-colors"><Trash2 size={16}/></button>
</div>
</div>
))}
</div>
</section>
</div>
</div>
{editingChallenge && (
<div className="fixed inset-0 bg-black/95 z-[400] flex items-center justify-center p-4">
<div className="hxp-border border-4 max-w-2xl w-full bg-black p-8 relative overflow-y-auto max-h-[90vh] custom-scrollbar">
<button onClick={() => setEditingChallenge(null)} className="absolute top-4 right-4 text-white hover:text-[#ff0000]"><X size={24}/></button>
<h3 className="text-3xl font-black italic text-white mb-8 uppercase">MANAGE_CHALLENGE</h3>
<form onSubmit={handleChallengeSubmit} className="space-y-4">
<input placeholder="TITLE" className="w-full bg-black hxp-border-purple p-3 text-white font-black uppercase" value={editingChallenge.title || ''} onChange={e => setEditingChallenge({...editingChallenge, title: e.target.value})} required />
<div className="grid grid-cols-3 gap-4">
<select className="bg-black hxp-border-purple p-3 text-white font-black uppercase" value={editingChallenge.category || 'WEB'} onChange={e => setEditingChallenge({...editingChallenge, category: e.target.value})}>
{CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
</select>
<select className="bg-black hxp-border-purple p-3 text-white font-black uppercase" value={editingChallenge.difficulty || 'Low'} onChange={e => setEditingChallenge({...editingChallenge, difficulty: e.target.value as Difficulty})}>
{DIFFICULTIES.map(d => <option key={d} value={d}>{d}</option>)}
</select>
<input type="number" placeholder="POINTS" className="bg-black hxp-border-purple p-3 text-white font-black" value={editingChallenge.initialPoints || ''} onChange={e => setEditingChallenge({...editingChallenge, initialPoints: parseInt(e.target.value)})} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-500 uppercase">Challenge Port</label>
<input type="number" placeholder="1337" className="w-full bg-black hxp-border-purple p-3 text-white font-black" value={editingChallenge.port || ''} onChange={e => setEditingChallenge({...editingChallenge, port: parseInt(e.target.value)})} />
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-slate-500 uppercase">Conn Protocol</label>
<select className="w-full bg-black hxp-border-purple p-3 text-white font-black uppercase" value={editingChallenge.connectionType || 'nc'} onChange={e => setEditingChallenge({...editingChallenge, connectionType: e.target.value as 'nc' | 'http'})}>
<option value="nc">Netcat (nc)</option>
<option value="http">HTTP/HTTPS</option>
</select>
</div>
</div>
<input placeholder="FLAG" className="w-full bg-black hxp-border-purple p-3 text-white font-black" value={editingChallenge.flag || ''} onChange={e => setEditingChallenge({...editingChallenge, flag: e.target.value})} required />
<textarea placeholder="DESCRIPTION" className="w-full bg-black hxp-border-purple p-3 text-white font-black h-32" value={editingChallenge.description || ''} onChange={e => setEditingChallenge({...editingChallenge, description: e.target.value})} />
<div className="space-y-2">
<label className="block text-[10px] font-black text-slate-500 uppercase tracking-widest">Manage Files</label>
{currentFiles.map((file, idx) => (
<div key={idx} className="flex items-center gap-2 p-2 bg-white/5 border border-white/10">
<Paperclip size={14} className="text-[#bf00ff]" />
<span className="text-xs flex-1 truncate">{file.name}</span>
<button type="button" onClick={() => removeExistingFile(idx)} className="text-red-500 hover:text-white"><Trash2 size={14}/></button>
</div>
))}
<input type="file" multiple onChange={e => e.target.files && setNewFiles(Array.from(e.target.files))} className="w-full bg-black hxp-border-purple p-3 text-white text-xs" />
</div>
<Button type="submit" className="w-full py-4 uppercase">Save Challenge</Button>
</form>
</div>
</div>
)}
{editingTeam && (
<div className="fixed inset-0 bg-black/95 z-[400] flex items-center justify-center p-4">
<div className="hxp-border border-4 max-w-md w-full bg-black p-8 relative">
<button onClick={() => setEditingTeam(null)} className="absolute top-4 right-4 text-white hover:text-[#ff0000]"><X size={24}/></button>
<h3 className="text-3xl font-black italic text-white mb-8 uppercase">UPDATE_OPERATOR</h3>
<form onSubmit={handleTeamUpdate} className="space-y-4">
<label className="block">
<span className="text-[10px] font-black text-slate-500 uppercase mb-2 block">Operator Name</span>
<input className="w-full bg-black hxp-border-purple p-3 text-white font-black" value={editingTeam.name || ''} onChange={e => setEditingTeam({...editingTeam, name: e.target.value})} required />
</label>
<label className="block">
<span className="text-[10px] font-black text-slate-500 uppercase mb-2 block">New Access Key (Optional)</span>
<input type="password" placeholder="LEAVE BLANK TO UNCHANGE" className="w-full bg-black hxp-border-purple p-3 text-white font-black" onChange={e => setEditingTeam({...editingTeam, newPassword: e.target.value})} />
</label>
<div className="flex gap-8 py-2">
<label className="flex items-center gap-2 cursor-pointer group">
<input type="checkbox" className="hidden" checked={!!editingTeam.isAdmin} onChange={e => setEditingTeam({...editingTeam, isAdmin: e.target.checked})} />
<div className={`w-5 h-5 border-2 flex items-center justify-center transition-colors ${editingTeam.isAdmin ? 'border-[#ffaa00] bg-[#ffaa00]' : 'border-slate-600'}`}>
{editingTeam.isAdmin && <Shield size={12} className="text-black" />}
</div>
<span className={`text-[10px] font-black uppercase ${editingTeam.isAdmin ? 'text-[#ffaa00]' : 'text-slate-500'}`}>Admin Role</span>
</label>
<label className="flex items-center gap-2 cursor-pointer group">
<input type="checkbox" className="hidden" checked={!!editingTeam.isDisabled} onChange={e => setEditingTeam({...editingTeam, isDisabled: e.target.checked})} />
<div className={`w-5 h-5 border-2 flex items-center justify-center transition-colors ${editingTeam.isDisabled ? 'border-[#ff0000] bg-[#ff0000]' : 'border-slate-600'}`}>
{editingTeam.isDisabled && <X size={12} className="text-black" />}
</div>
<span className={`text-[10px] font-black uppercase ${editingTeam.isDisabled ? 'text-[#ff0000]' : 'text-slate-500'}`}>Disabled</span>
</label>
</div>
<Button type="submit" className="w-full py-4 uppercase">Commit Identity Changes</Button>
</form>
</div>
</div>
)}
{editingBlogPost && (
<div className="fixed inset-0 bg-black/95 z-[400] flex items-center justify-center p-4">
<div className="hxp-border border-4 max-w-2xl w-full bg-black p-8 relative">
<button onClick={() => setEditingBlogPost(null)} className="absolute top-4 right-4 text-white hover:text-[#ff0000]"><X size={24}/></button>
<h3 className="text-3xl font-black italic text-white mb-8 uppercase">BLOG_PROTOCOL</h3>
<form onSubmit={handleBlogSubmit} className="space-y-4">
<input placeholder="HEADLINE" className="w-full bg-black hxp-border-purple p-3 text-white font-black uppercase" value={editingBlogPost.title || ''} onChange={e => setEditingBlogPost({...editingBlogPost, title: e.target.value})} required />
<textarea placeholder="CONTENT" className="w-full bg-black hxp-border-purple p-3 text-white font-black h-48" value={editingBlogPost.content || ''} onChange={e => setEditingBlogPost({...editingBlogPost, content: e.target.value})} required />
<Button type="submit" className="w-full py-4 uppercase">Publish Broadcast</Button>
</form>
</div>
</div>
)}
</div>
);
};
const App: React.FC = () => {
const [state, setState] = useState<CTFState>({
isStarted: false,
startTime: null,
teams: [],
challenges: [],
solves: [],
blogs: [],
config: {}
});
const [currentUser, setCurrentUser] = useState<Team | null>(null);
const [loading, setLoading] = useState(true);
const [showProfileModal, setShowProfileModal] = useState(false);
const refreshState = async () => {
try {
const newState = await api.getState();
setState(newState);
} catch (err) {
console.error("Failed to fetch state", err);
}
};
useEffect(() => {
const init = async () => {
const session = localStorage.getItem('hip6_session');
if (session) {
try {
const { team } = JSON.parse(session);
setCurrentUser(team);
} catch (e) {}
}
await refreshState();
setLoading(false);
};
init();
const interval = setInterval(refreshState, 30000);
return () => clearInterval(interval);
}, []);
const login = async (name: string, pass: string) => {
try {
const { team, token } = await api.login(name, pass);
localStorage.setItem('hip6_session', JSON.stringify({ team, token }));
setCurrentUser(team);
await refreshState();
return true;
} catch (err) { return false; }
};
const register = async (name: string, pass: string) => {
const { team, token } = await api.register(name, pass);
localStorage.setItem('hip6_session', JSON.stringify({ team, token }));
setCurrentUser(team);
await refreshState();
};
const logout = () => {
localStorage.removeItem('hip6_session');
setCurrentUser(null);
};
const submitFlag = async (challengeId: string, flag: string) => {
const res = await api.submitFlag(challengeId, flag);
if (res.success) { await refreshState(); return true; }
return false;
};
const toggleCtf = async () => { await api.toggleCtf(); await refreshState(); };
const upsertChallenge = async (data: FormData, id?: string) => { await api.upsertChallenge(data, id); await refreshState(); };
const deleteChallenge = async (id: string) => { await api.deleteChallenge(id); await refreshState(); };
const updateTeam = async (id: string, name: string, isDisabled: boolean, isAdmin: boolean, password?: string) => { await api.updateTeam(id, { name, isDisabled, isAdmin, password }); await refreshState(); };
const updateProfile = async (password?: string) => { await api.updateProfile({ password }); await refreshState(); };
const deleteTeam = async (id: string) => { await api.deleteTeam(id); await refreshState(); };
const createBlogPost = async (data: {title: string, content: string}) => { await api.createBlogPost(data); await refreshState(); };
const updateBlogPost = async (id: string, data: {title: string, content: string}) => { await api.updateBlogPost(id, data); await refreshState(); };
const deleteBlogPost = async (id: string) => { await api.deleteBlogPost(id); await refreshState(); };
const updateConfig = async (formData: FormData) => { await api.updateConfig(formData); await refreshState(); };
if (loading) {
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="text-[#ff0000] font-black italic text-4xl animate-pulse tracking-tighter">LOADING_CTF_PROTOCOLS...</div>
</div>
);
}
const bgStyles: React.CSSProperties = {
backgroundColor: state.config.bgType === 'color' ? (state.config.bgColor || '#000000') : '#000000'
};
return (
<CTFContext.Provider value={{
state, currentUser, login, register, logout, submitFlag,
toggleCtf, upsertChallenge, deleteChallenge, updateTeam, updateProfile, deleteTeam,
createBlogPost, updateBlogPost, deleteBlogPost, updateConfig, refreshState
}}>
<HashRouter>
<div style={bgStyles} className="min-h-screen text-white font-mono selection:bg-[#ff0000] selection:text-black relative overflow-x-hidden">
{/* Background Layer */}
{state.config.bgType === 'image' && state.config.bgImageUrl && (
<div
className="fixed inset-0 pointer-events-none z-0"
style={{
backgroundImage: `url(${state.config.bgImageUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundAttachment: 'fixed',
opacity: parseFloat(state.config.bgOpacity || '0.5'),
filter: `brightness(${state.config.bgBrightness || '1.0'}) contrast(${state.config.bgContrast || '1.0'})`
}}
/>
)}
<nav className="border-b-4 border-[#333] px-6 py-4 flex justify-between items-center sticky top-0 bg-black/80 backdrop-blur-md z-[100]">
<Link to="/" className="flex items-center gap-3 group">
{state.config.logoUrl ? (
<img src={state.config.logoUrl} alt="Logo" className="w-8 h-8 object-contain" />
) : (
<Terminal className="w-8 h-8 text-[#ff0000]" />
)}
<span className="text-2xl font-black italic tracking-tighter group-hover:text-[#ff0000] transition-colors uppercase">
{state.config.conferenceName}
</span>
</Link>
<div className="hidden md:flex items-center gap-8 text-xs font-black tracking-widest uppercase">
<Link to="/challenges" className="hover:text-[#bf00ff] transition-colors flex items-center gap-2"><Flag size={14}/> Challenges</Link>
<Link to="/blog" className="hover:text-[#bf00ff] transition-colors flex items-center gap-2"><Newspaper size={14}/> Blog</Link>
<Link to="/scoreboard" className="hover:text-[#bf00ff] transition-colors flex items-center gap-2"><Trophy size={14}/> Scoreboard</Link>
{currentUser?.isAdmin && (
<Link to="/admin" className="text-[#ff0000] hover:text-white transition-colors flex items-center gap-2"><Shield size={14}/> Admin</Link>
)}
</div>
<div className="flex items-center gap-4">
{currentUser ? (
<div className="flex items-center gap-4">
<div className="text-right hidden sm:block">
<p className="text-[10px] text-slate-500 font-bold leading-none uppercase">OPERATOR</p>
<p className="text-sm font-black italic">{currentUser.name}</p>
</div>
<button onClick={() => setShowProfileModal(true)} title="Profile Settings" className="p-2 border-2 border-[#bf00ff] text-[#bf00ff] hover:bg-[#bf00ff] hover:text-black transition-all">
<Settings size={20} />
</button>
<button onClick={logout} title="Sign Out" className="p-2 border-2 border-[#ff0000] text-[#ff0000] hover:bg-[#ff0000] hover:text-black transition-all">
<LogOut size={20} />
</button>
</div>
) : (
<Link to="/login">
<Button variant="secondary" className="flex items-center gap-2 text-xs">
<LogIn size={16} /> SIGN_IN
</Button>
</Link>
)}
</div>
</nav>
<main className="relative z-10">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/challenges" element={<ProtectedRoute><ChallengeList /></ProtectedRoute>} />
<Route path="/blog" element={<Blog />} />
<Route path="/scoreboard" element={<Scoreboard />} />
<Route path="/matrix" element={<ScoreMatrix />} />
<Route path="/admin" element={<ProtectedRoute>{currentUser?.isAdmin ? <Admin /> : <Navigate to="/" />}</ProtectedRoute>} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</main>
{showProfileModal && <ProfileSettingsModal onClose={() => setShowProfileModal(false)} />}
</div>
</HashRouter>
</CTFContext.Provider>
);
};
export default App;