- 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:
59
Admin.tsx
59
Admin.tsx
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -21,6 +21,7 @@ interface CTFContextType {
|
||||
updateTeam: (id: string, data: any) => Promise<void>;
|
||||
updateProfile: (password?: string) => Promise<void>;
|
||||
deleteTeam: (id: string) => Promise<void>;
|
||||
deleteSolve: (teamId: string, challengeId: 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>;
|
||||
@@ -98,6 +99,7 @@ export const CTFProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const updateTeam = async (id: string, d: any) => { await api.updateTeam(id, d); await refreshState(); };
|
||||
const updateProfile = async (p?: string) => { await api.updateProfile({ password: p }); await refreshState(); };
|
||||
const deleteTeam = async (id: string) => { if (window.confirm("EXPEL_OPERATOR?")) { await api.deleteTeam(id); await refreshState(); } };
|
||||
const deleteSolve = async (teamId: string, challengeId: string) => { if (window.confirm("DELETE_SOLVE?")) { await api.deleteSolve(teamId, challengeId); await refreshState(); } };
|
||||
const createBlogPost = async (d: any) => { await api.createBlogPost(d); await refreshState(); };
|
||||
const updateBlogPost = async (id: string, d: any) => { await api.updateBlogPost(id, d); await refreshState(); };
|
||||
const deleteBlogPost = async (id: string) => { await api.deleteBlogPost(id); await refreshState(); };
|
||||
@@ -108,7 +110,7 @@ export const CTFProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
state, currentUser, login, register, logout, submitFlag, toggleCtf, resetScores,
|
||||
upsertChallenge, deleteChallenge, deleteAllChallenges, exportChallenges,
|
||||
importChallenges, backupDatabase, restoreDatabase, updateTeam, updateProfile,
|
||||
deleteTeam, createBlogPost, updateBlogPost, deleteBlogPost, updateConfig,
|
||||
deleteTeam, deleteSolve, createBlogPost, updateBlogPost, deleteBlogPost, updateConfig,
|
||||
refreshState, loading, loadError
|
||||
}}>
|
||||
{children}
|
||||
|
||||
37
changelog.txt
Normal file
37
changelog.txt
Normal file
@@ -0,0 +1,37 @@
|
||||
2026-03-07
|
||||
- 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
|
||||
|
||||
2026-02-28
|
||||
- Removed the UTC time display from the countdown, leaving only the CET time
|
||||
- Added logic to display the event start time in Central European Time (CET) on the Challenges list page
|
||||
- Replaced mock authentication tokens with secure JWT-like signed tokens
|
||||
- Added robust error handling and type checking for password hashing and validation functions
|
||||
- Implemented logic to disable the default admin account (admin-0) once another admin is created
|
||||
- Applied a database schema whitelist to prevent SQL injection during database restores
|
||||
- Filtered out admin and disabled teams from the public scoreboard state for non-admin users
|
||||
- Added strict /admin middleware to protect administrative API endpoints by verifying user permissions
|
||||
- Updated page title to HIP7CTF in index.html
|
||||
- Enhanced the state endpoint to completely hide challenges if the current time is before the configured event start time
|
||||
|
||||
2026-02-22
|
||||
- Modified the /state endpoint to return an empty challenges list if the event has not started and the user is not an admin
|
||||
|
||||
2026-02-05
|
||||
- Included authorization headers in the frontend getState API requests
|
||||
- Added security check in the /state endpoint to filter out the flag from challenge data for non-admin users
|
||||
- Added a dbGet utility function to the server
|
||||
|
||||
2026-01-21
|
||||
- Removed the README.md file
|
||||
- Added a fix-permissions service to docker-compose.yml to automatically set correct ownership for data and uploads directories
|
||||
- Modularized the frontend by splitting the monolithic App.tsx into dedicated components (Admin.tsx, Auth.tsx, Blog.tsx, CTFContext.tsx, Challenges.tsx, Home.tsx, Scoreboard.tsx, UIComponents.tsx)
|
||||
- Restructured API, server, and scoring logic to fit the new modular frontend architecture
|
||||
- Re-added README.md file temporarily
|
||||
|
||||
2026-01-07
|
||||
- Removed the README.md file
|
||||
- Initial project setup including React frontend, Express backend, Docker configuration, and baseline scoring services
|
||||
1
data/secret.key
Normal file
1
data/secret.key
Normal file
@@ -0,0 +1 @@
|
||||
3ca2310b67c3e5a157860fa2a5a3d381f34925ed8ec7988b44ddd7d39506aaf8
|
||||
12
server.js
12
server.js
@@ -429,11 +429,15 @@ apiRouter.post('/challenges/submit', (req, res) => {
|
||||
configRows.forEach(row => { config[row.key] = row.value; });
|
||||
const now = Date.now(), start = parseInt(config.eventStartTime || 0), end = parseInt(config.eventEndTime || Date.now() + 86400000);
|
||||
if (config.isStarted !== 'true' || now < start || now > end) return res.status(403).json({ success: false, message: 'COMPETITION_NOT_ACTIVE' });
|
||||
db.get("SELECT isDisabled FROM teams WHERE id = ?", [teamId], (err, team) => {
|
||||
db.get("SELECT isDisabled, isAdmin FROM teams WHERE id = ?", [teamId], (err, team) => {
|
||||
if (team?.isDisabled) return res.status(403).json({ success: false, message: 'Account disabled' });
|
||||
db.get("SELECT * FROM challenges WHERE id = ?", [challengeId], (err, challenge) => {
|
||||
if (challenge && challenge.flag === flag) {
|
||||
db.run("INSERT OR IGNORE INTO solves (teamId, challengeId, timestamp) VALUES (?, ?, ?)", [teamId, challengeId, Date.now()], () => res.json({ success: true }));
|
||||
if (team?.isAdmin) {
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
db.run("INSERT OR IGNORE INTO solves (teamId, challengeId, timestamp) VALUES (?, ?, ?)", [teamId, challengeId, Date.now()], () => res.json({ success: true }));
|
||||
}
|
||||
} else res.json({ success: false });
|
||||
});
|
||||
});
|
||||
@@ -478,6 +482,10 @@ apiRouter.delete('/admin/teams/:id', (req, res) => {
|
||||
db.run("DELETE FROM teams WHERE id = ?", [req.params.id], () => db.run("DELETE FROM solves WHERE teamId = ?", [req.params.id], () => res.json({ success: true })));
|
||||
});
|
||||
|
||||
apiRouter.delete('/admin/solves/:teamId/:challengeId', (req, res) => {
|
||||
db.run("DELETE FROM solves WHERE teamId = ? AND challengeId = ?", [req.params.teamId, req.params.challengeId], () => res.json({ success: true }));
|
||||
});
|
||||
|
||||
apiRouter.post('/admin/blogs', (req, res) => {
|
||||
const id = 'blog-' + Math.random().toString(36).substr(2, 9);
|
||||
db.run("INSERT INTO blogs (id, title, content, timestamp) VALUES (?, ?, ?, ?)", [id, req.body.title, req.body.content, Date.now()], () => res.json({ success: true, id }));
|
||||
|
||||
@@ -134,11 +134,17 @@ class ApiService {
|
||||
}
|
||||
|
||||
async deleteTeam(id: string): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/admin/teams/${id}`, {
|
||||
await fetch(`${API_BASE}/admin/teams/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: this.getHeaders(),
|
||||
headers: { ...this.getHeaders() }
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSolve(teamId: string, challengeId: string): Promise<void> {
|
||||
await fetch(`${API_BASE}/admin/solves/${teamId}/${challengeId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { ...this.getHeaders() }
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to delete team');
|
||||
}
|
||||
|
||||
async submitFlag(challengeId: string, flag: string): Promise<{ success: boolean }> {
|
||||
|
||||
Reference in New Issue
Block a user