- Prevented admin challenge solves from creating score records

- Added operator solves list to the Admin panel profile
- Allowed deletion of specific operator solves from the Admin panel
- Enhanced operator solves list with alphabetical sorting, difficulty colors, and point values
- Added rank medal icons to operator solves in the Admin panel
This commit is contained in:
m0rph3us1987
2026-03-07 02:18:47 +01:00
parent e04547301b
commit 800192c87f
6 changed files with 117 additions and 8 deletions

View File

@@ -1,13 +1,14 @@
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 { X, Edit3, Trash2, Shield, ShieldCheck, ShieldAlert, Skull, Newspaper, Download, Upload, Database, Save, History, Plus, Globe, User, ShieldX, UserMinus, UserCheck, Medal, CheckCircle2 } 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';
import { calculateChallengeValue, getFirstBloodBonusFactor } from './services/scoring';
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 { state, toggleCtf, resetScores, upsertChallenge, deleteChallenge, deleteAllChallenges, updateTeam, deleteTeam, deleteSolve, createBlogPost, updateBlogPost, deleteBlogPost, updateConfig, exportChallenges, importChallenges, backupDatabase, restoreDatabase, refreshState } = useCTF();
const [editingChallenge, setEditingChallenge] = useState<Partial<Challenge> | null>(null);
const [editingTeam, setEditingTeam] = useState<Partial<Team> & { newPassword?: string } | null>(null);
@@ -460,6 +461,60 @@ export const Admin: React.FC = () => {
</div>
<Button type="submit" className="w-full py-4 uppercase">Update Identity</Button>
</form>
<div className="mt-8 border-t-2 border-[#333] pt-6">
<h4 className="text-xl font-black italic text-[#bf00ff] mb-4 uppercase">SOLVES</h4>
<div className="space-y-2 max-h-48 overflow-y-auto pr-2">
{state.solves.filter(s => s.teamId === editingTeam.id).length === 0 ? (
<p className="text-slate-500 italic text-sm font-black">NO_SOLVES_FOUND</p>
) : (
state.solves
.filter(s => s.teamId === editingTeam.id)
.map(solve => ({
solve,
challenge: state.challenges.find(c => c.id === solve.challengeId)
}))
.sort((a, b) => (a.challenge?.title || '').localeCompare(b.challenge?.title || ''))
.map(({ solve, challenge }) => {
if (!challenge) return null;
const diffColor = challenge.difficulty === 'Low' ? 'text-[#00ff00]' : challenge.difficulty === 'Medium' ? 'text-[#ffaa00]' : 'text-[#ff0000]';
const challengeSolves = state.solves
.filter(s => s.challengeId === challenge.id)
.sort((a, b) => a.timestamp - b.timestamp);
const rank = challengeSolves.findIndex(s => s.teamId === editingTeam.id);
const baseValue = calculateChallengeValue(
challenge.initialPoints,
challenge.minimumPoints || 0,
challenge.decaySolves || 1,
(challenge.solves || []).length
);
const bonus = Math.floor(challenge.initialPoints * getFirstBloodBonusFactor(rank));
const totalPoints = baseValue + bonus;
return (
<div key={solve.challengeId} className="flex justify-between items-center bg-white/5 border border-[#333] p-3 text-sm font-bold">
<div className="flex items-center gap-2 truncate flex-1">
{rank === 0 ? (
<Medal size={16} className="text-[#ffaa00]" />
) : rank === 1 ? (
<Medal size={16} className="text-slate-400" />
) : rank === 2 ? (
<Medal size={16} className="text-[#cd7f32]" />
) : (
<CheckCircle2 size={14} className="text-[#00ff00]" />
)}
<span className={`truncate ${diffColor}`}>{challenge.title}</span>
</div>
<span className="text-white text-xs mr-4">{totalPoints} PTS</span>
<button type="button" onClick={() => deleteSolve(editingTeam.id as string, solve.challengeId)} className="text-red-500 hover:text-red-400 transition-colors" title="Delete Solve"><Trash2 size={16}/></button>
</div>
);
})
)}
</div>
</div>
</div>
</div>
)}