From 40f496c3f2edd3fbb901dfed8fa21405e1bd4327 Mon Sep 17 00:00:00 2001 From: m0rph3us1987 Date: Wed, 21 Jan 2026 18:59:14 +0100 Subject: [PATCH] Made app more modular. Fixed some bugs. Added some functionality. --- Admin.tsx | 482 ++++++++++++++++ App.tsx | 1290 +++---------------------------------------- Auth.tsx | 127 +++++ Blog.tsx | 22 + CTFContext.tsx | 117 ++++ Challenges.tsx | 221 ++++++++ Home.tsx | 59 ++ README.md | 20 + Scoreboard.tsx | 165 ++++++ UIComponents.tsx | 67 +++ constants.ts | 13 +- docker-compose.yml | 12 +- metadata.json | 4 +- package.json | 2 +- server.js | 461 ++++++++-------- services/api.ts | 123 +++-- services/scoring.ts | 54 +- types.ts | 5 +- 18 files changed, 1709 insertions(+), 1535 deletions(-) create mode 100644 Admin.tsx create mode 100644 Auth.tsx create mode 100644 Blog.tsx create mode 100644 CTFContext.tsx create mode 100644 Challenges.tsx create mode 100644 Home.tsx create mode 100644 README.md create mode 100644 Scoreboard.tsx create mode 100644 UIComponents.tsx diff --git a/Admin.tsx b/Admin.tsx new file mode 100644 index 0000000..506ad98 --- /dev/null +++ b/Admin.tsx @@ -0,0 +1,482 @@ + +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import { X, Edit3, Trash2, Shield, ShieldCheck, ShieldAlert, Skull, Newspaper, Download, Upload, Database, Save, History, Plus, Globe, User, ShieldX, UserMinus, UserCheck } from 'lucide-react'; +import { useCTF } from './CTFContext'; +import { Challenge, Team, BlogPost, Difficulty, ChallengeFile } from './types'; +import { Button, Countdown, CategoryIcon } from './UIComponents'; +import { CATEGORIES, DIFFICULTIES } from './constants'; + +export const Admin: React.FC = () => { + const { state, toggleCtf, resetScores, upsertChallenge, deleteChallenge, deleteAllChallenges, updateTeam, deleteTeam, createBlogPost, updateBlogPost, deleteBlogPost, updateConfig, exportChallenges, importChallenges, backupDatabase, restoreDatabase, refreshState } = useCTF(); + + const [editingChallenge, setEditingChallenge] = useState | null>(null); + const [editingTeam, setEditingTeam] = useState & { newPassword?: string } | null>(null); + const [editingBlogPost, setEditingBlogPost] = useState | null>(null); + const [newFiles, setNewFiles] = useState([]); + const [currentFiles, setCurrentFiles] = useState([]); + const [localConf, setLocalConf] = useState>({}); + const initialLoadDone = useRef(false); + + // Sorted data for display + const sortedChallenges = useMemo(() => { + return [...state.challenges].sort((a, b) => a.title.localeCompare(b.title)); + }, [state.challenges]); + + const sortedOperators = useMemo(() => { + return state.teams + .filter(t => t.id !== 'admin-0') + .sort((a, b) => a.name.localeCompare(b.name)); + }, [state.teams]); + + useEffect(() => { + if (Object.keys(state.config).length > 0 && !initialLoadDone.current) { + setLocalConf({ ...state.config }); + initialLoadDone.current = true; + } + }, [state.config]); + + const importChalRef = useRef(null); + const restoreDbRef = useRef(null); + + const handleConfigSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const fd = new FormData(); + Object.entries(localConf).forEach(([k, v]) => { + if (v !== undefined && v !== null && v !== 'NaN') fd.append(k, String(v)); + }); + const logoInput = (e.target as any).logo; + const bgInput = (e.target as any).bgImage; + if (logoInput && logoInput.files[0]) fd.append('logo', logoInput.files[0]); + if (bgInput && bgInput.files[0]) fd.append('bgImage', bgInput.files[0]); + try { + await updateConfig(fd); + await refreshState(); + initialLoadDone.current = false; + alert('CONFIGURATION_SAVED_SUCCESSFULLY'); + } catch (err) { alert('SAVE_FAILED_CHECK_CONSOLE'); } + }; + + const toUTCDisplay = (msStr: string) => { + if (!msStr || msStr === 'undefined' || msStr === 'NaN') return ""; + const ms = parseInt(msStr); + if (isNaN(ms)) return ""; + const d = new Date(ms); + return d.toISOString().slice(0, 16); + }; + + const fromUTCDisplay = (isoStr: string) => { + if (!isoStr) return ""; + const d = new Date(isoStr + ":00Z"); + const ms = d.getTime(); + return isNaN(ms) ? "" : ms.toString(); + }; + + const eventStartTime = parseInt(state.config.eventStartTime || "0"); + const isBeforeStart = Date.now() < eventStartTime; + + const handleDifficultyChange = (val: Difficulty) => { + if (!editingChallenge) return; + const points = val === 'Low' ? 100 : val === 'Medium' ? 200 : 300; + setEditingChallenge({ + ...editingChallenge, + difficulty: val, + initialPoints: points, + minimumPoints: Math.floor(points / 2) + }); + }; + + const handleInitialPointsChange = (p: number) => { + if (!editingChallenge) return; + setEditingChallenge({ + ...editingChallenge, + initialPoints: p, + minimumPoints: Math.floor(p / 2) + }); + }; + + const quickToggleAdmin = async (team: Team) => { + if (team.id === 'admin-0') return; + await updateTeam(team.id, { ...team, isAdmin: !team.isAdmin }); + }; + + const quickToggleStatus = async (team: Team) => { + if (team.id === 'admin-0') return; + await updateTeam(team.id, { ...team, isDisabled: !team.isDisabled }); + }; + + return ( +
+
+

ADMIN_CONSOLE

+
+ {isBeforeStart && ( +
+ +
+ )} + +
+
+
+
+
+

GENERAL_CONFIG

+
+
+
+ + setLocalConf({...localConf, conferenceName: e.target.value})} /> +
+
+
+ + { const val = fromUTCDisplay(e.target.value); if (val) setLocalConf({...localConf, eventStartTime: val}); }} /> +
+
+ + { const val = fromUTCDisplay(e.target.value); if (val) setLocalConf({...localConf, eventEndTime: val}); }} /> +
+
+
+ + setLocalConf({...localConf, dockerIp: e.target.value})} /> +
+
+ +