Made app more modular.

Fixed some bugs.
Added some functionality.
This commit is contained in:
m0rph3us1987
2026-01-21 18:59:14 +01:00
parent 5802b80d61
commit 40f496c3f2
18 changed files with 1709 additions and 1535 deletions

461
server.js
View File

@@ -5,14 +5,39 @@ const multer = require('multer');
const path = require('path');
const fs = require('fs');
const cors = require('cors');
const crypto = require('crypto');
const app = express();
const port = process.env.PORT || 3000;
// Password Hashing Helpers
function hashPassword(password) {
const salt = crypto.randomBytes(16).toString('hex');
const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');
return `${salt}:${hash}`;
}
function comparePassword(password, storedPassword) {
if (!storedPassword || !storedPassword.includes(':')) return false;
const [salt, hash] = storedPassword.split(':');
const checkHash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');
return hash === checkHash;
}
// Initialize Database
const db = new sqlite3.Database('./ctf.db', (err) => {
const db = new sqlite3.Database('./data/ctf.db', (err) => {
if (err) console.error('Database connection error:', err.message);
else console.log('Connected to the ctf.db SQLite database.');
else {
console.log('Connected to the ctf.db SQLite database.');
db.run("PRAGMA foreign_keys = ON");
}
});
const dbAll = (sql, params = []) => new Promise((resolve, reject) => {
db.all(sql, params, (err, rows) => {
if (err) reject(err);
else resolve(rows || []);
});
});
// Setup Storage
@@ -27,7 +52,6 @@ const upload = multer({ storage });
// Database Schema & Migrations
db.serialize(() => {
// Ensure tables exist
db.run(`CREATE TABLE IF NOT EXISTS teams (
id TEXT PRIMARY KEY,
name TEXT UNIQUE COLLATE NOCASE,
@@ -43,29 +67,32 @@ db.serialize(() => {
difficulty TEXT,
description TEXT,
initialPoints INTEGER,
minimumPoints INTEGER DEFAULT 0,
decaySolves INTEGER DEFAULT 1,
flag TEXT,
files TEXT DEFAULT '[]',
port INTEGER,
connectionType TEXT
connectionType TEXT,
overrideIp TEXT
)`);
// Force migration check for existing columns
db.all("PRAGMA table_info(challenges)", (err, rows) => {
if (err) return;
const columns = rows.map(r => r.name);
if (!columns.includes('port')) {
db.run("ALTER TABLE challenges ADD COLUMN port INTEGER DEFAULT 0");
}
if (!columns.includes('connectionType')) {
db.run("ALTER TABLE challenges ADD COLUMN connectionType TEXT DEFAULT 'nc'");
}
if (!columns.includes('port')) db.run("ALTER TABLE challenges ADD COLUMN port INTEGER DEFAULT 0");
if (!columns.includes('connectionType')) db.run("ALTER TABLE challenges ADD COLUMN connectionType TEXT DEFAULT 'nc'");
if (!columns.includes('overrideIp')) db.run("ALTER TABLE challenges ADD COLUMN overrideIp TEXT");
if (!columns.includes('minimumPoints')) db.run("ALTER TABLE challenges ADD COLUMN minimumPoints INTEGER DEFAULT 0");
if (!columns.includes('decaySolves')) db.run("ALTER TABLE challenges ADD COLUMN decaySolves INTEGER DEFAULT 1");
});
db.run(`CREATE TABLE IF NOT EXISTS solves (
teamId TEXT,
challengeId TEXT,
timestamp INTEGER,
PRIMARY KEY (teamId, challengeId)
PRIMARY KEY (teamId, challengeId),
FOREIGN KEY (teamId) REFERENCES teams(id) ON DELETE CASCADE,
FOREIGN KEY (challengeId) REFERENCES challenges(id) ON DELETE CASCADE
)`);
db.run(`CREATE TABLE IF NOT EXISTS blogs (
@@ -80,55 +107,86 @@ db.serialize(() => {
value TEXT
)`);
// Default Configs - Updated with Docker IP
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('isStarted', 'false')`);
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('conferenceName', 'HIP')`);
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('landingText', 'WELCOME TO THE PLAYGROUND. SOLVE CHALLENGES. SHARE KNOWLEDGE. 🦄')`);
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('logoUrl', '')`);
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('bgType', 'color')`);
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('bgColor', '#000000')`);
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('bgImageUrl', '')`);
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('bgOpacity', '0.5')`);
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('bgBrightness', '1.0')`);
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('bgContrast', '1.0')`);
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES ('dockerIp', '127.0.0.1')`);
const defaults = [
['isStarted', 'false'],
['conferenceName', 'HIP'],
['landingText', 'WELCOME TO THE PLAYGROUND. SOLVE CHALLENGES. SHARE KNOWLEDGE. 🦄'],
['logoData', ''],
['bgType', 'color'],
['bgColor', '#000000'],
['bgImageData', ''],
['bgOpacity', '0.5'],
['bgBrightness', '1.0'],
['bgContrast', '1.0'],
['dockerIp', '127.0.0.1'],
['eventStartTime', Date.now().toString()],
['eventEndTime', (Date.now() + 86400000).toString()]
];
db.run(`INSERT OR IGNORE INTO teams (id, name, password, isAdmin, isDisabled) VALUES ('admin-0', 'admin', 'admin', 1, 0)`);
defaults.forEach(([k, v]) => {
db.run(`INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)`, [k, v]);
});
const adminPass = hashPassword('admin');
db.run(`INSERT OR IGNORE INTO teams (id, name, password, isAdmin, isDisabled) VALUES ('admin-0', 'admin', ?, 1, 0)`, [adminPass]);
});
// Middleware
const unlinkFiles = (filesJson) => {
try {
const files = JSON.parse(filesJson || '[]');
files.forEach(f => {
const fileName = path.basename(f.url);
const filePath = path.join(uploadDir, fileName);
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
});
} catch (e) { console.error("Error unlinking files:", e); }
};
app.use(cors());
app.use(express.json());
app.use(express.json({ limit: '500mb' }));
app.use('/files', express.static(uploadDir));
// API Router
const apiRouter = express.Router();
// State endpoint
apiRouter.post('/auth/register', (req, res) => {
const { name, password } = req.body;
if (!name || !password) return res.status(400).json({ message: 'Missing credentials' });
const id = 'team-' + Math.random().toString(36).substr(2, 9);
const hashedPass = hashPassword(password);
db.run("INSERT INTO teams (id, name, password, isAdmin, isDisabled) VALUES (?, ?, ?, 0, 0)", [id, name, hashedPass], function(err) {
if (err) {
if (err.message.includes('UNIQUE')) return res.status(400).json({ message: 'Team name already exists' });
return res.status(500).json({ message: 'Registration failed' });
}
res.json({ team: { id, name, isAdmin: 0, isDisabled: 0 }, token: `mock-token-${id}` });
});
});
apiRouter.post('/auth/login', (req, res) => {
const { name, password } = req.body;
db.get("SELECT * FROM teams WHERE name = ?", [name], (err, team) => {
if (err || !team || !comparePassword(password, team.password)) return res.status(401).json({ message: 'Invalid credentials' });
if (team.isDisabled) return res.status(403).json({ message: 'Account disabled' });
const { password: _, ...teamData } = team;
res.json({ team: teamData, token: `mock-token-${team.id}` });
});
});
apiRouter.get('/state', (req, res) => {
const state = { isStarted: false, teams: [], challenges: [], solves: [], blogs: [], config: {} };
db.all("SELECT key, value FROM config", (err, configRows) => {
if (err) return res.status(500).json({ error: 'Failed to fetch config' });
configRows.forEach(row => {
state.config[row.key] = row.value;
});
configRows.forEach(row => { state.config[row.key] = row.value; });
state.isStarted = state.config.isStarted === 'true';
db.all("SELECT id, name, isAdmin, isDisabled FROM teams", (err, teams) => {
if (err) return res.status(500).json({ error: 'Failed to fetch teams' });
state.teams = teams || [];
db.all("SELECT * FROM challenges", (err, challenges) => {
if (err) return res.status(500).json({ error: 'Failed to fetch challenges' });
db.all("SELECT * FROM solves", (err, solves) => {
if (err) return res.status(500).json({ error: 'Failed to fetch solves' });
db.all("SELECT * FROM blogs ORDER BY timestamp DESC", (err, blogs) => {
if (err) return res.status(500).json({ error: 'Failed to fetch blogs' });
state.solves = solves || [];
state.blogs = blogs || [];
state.challenges = (challenges || []).map(c => ({
@@ -144,262 +202,193 @@ apiRouter.get('/state', (req, res) => {
});
});
// Auth
apiRouter.post('/auth/register', (req, res) => {
const { name, password } = req.body;
if (!name || !password) return res.status(400).json({ message: 'Name and password required' });
const id = 'team-' + Math.random().toString(36).substr(2, 9);
db.run("INSERT INTO teams (id, name, password, isAdmin, isDisabled) VALUES (?, ?, ?, 0, 0)", [id, name, password], function(err) {
if (err) {
if (err.message.includes('UNIQUE constraint failed')) {
return res.status(400).json({ message: 'This team name is already taken.' });
}
return res.status(500).json({ message: 'Internal server error during registration.' });
}
res.json({ team: { id, name, isAdmin: false, isDisabled: false }, token: 'mock-token-' + id });
});
});
apiRouter.post('/auth/login', (req, res) => {
const { name, password } = req.body;
db.get("SELECT * FROM teams WHERE name = ? AND password = ?", [name, password], (err, row) => {
if (err || !row) return res.status(401).json({ message: 'Invalid credentials' });
if (row.isDisabled) return res.status(403).json({ message: 'Account disabled' });
res.json({ team: { id: row.id, name: row.name, isAdmin: !!row.isAdmin, isDisabled: !!row.isDisabled }, token: 'mock-token-' + row.id });
});
});
// Update Config
apiRouter.put('/admin/config', upload.fields([{ name: 'logo' }, { name: 'bgImage' }]), (req, res) => {
const updates = { ...req.body };
if (req.files) {
if (req.files.logo) updates.logoUrl = `/files/${req.files.logo[0].filename}`;
if (req.files.bgImage) updates.bgImageUrl = `/files/${req.files.bgImage[0].filename}`;
if (req.files.logo) {
const file = req.files.logo[0];
const data = fs.readFileSync(file.path).toString('base64');
updates.logoData = `data:${file.mimetype};base64,${data}`;
fs.unlinkSync(file.path);
}
if (req.files.bgImage) {
const file = req.files.bgImage[0];
const data = fs.readFileSync(file.path).toString('base64');
updates.bgImageData = `data:${file.mimetype};base64,${data}`;
fs.unlinkSync(file.path);
}
}
db.serialize(() => {
db.run("BEGIN TRANSACTION");
const stmt = db.prepare("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)");
Object.entries(updates).forEach(([key, value]) => {
stmt.run(key, value);
if (!['logo','bgImage','files','isStarted'].includes(key)) stmt.run(key, String(value));
});
stmt.finalize(() => {
stmt.finalize();
db.run("COMMIT", (err) => {
if (err) return res.status(500).json({ success: false });
res.json({ success: true });
});
});
});
// Challenges Submit
apiRouter.get('/admin/db/export', async (req, res) => {
try {
const backup = {};
const tables = ['teams', 'challenges', 'solves', 'blogs', 'config'];
for (const table of tables) {
const rows = await dbAll(`SELECT * FROM ${table}`);
if (table === 'challenges') {
for (const challenge of rows) {
const files = JSON.parse(challenge.files || '[]');
const enrichedFiles = files.map(file => {
const filePath = path.join(uploadDir, path.basename(file.url));
if (fs.existsSync(filePath)) return { ...file, base64: fs.readFileSync(filePath).toString('base64') };
return file;
});
challenge.files = JSON.stringify(enrichedFiles);
}
}
backup[table] = rows;
}
res.json(backup);
} catch (err) { res.status(500).json({ error: 'EXPORT_FAILED' }); }
});
apiRouter.post('/admin/db/restore', upload.single('restoreFile'), (req, res) => {
if (!req.file) return res.status(400).json({ message: 'No file' });
try {
const data = JSON.parse(fs.readFileSync(req.file.path, 'utf8'));
fs.unlinkSync(req.file.path);
db.serialize(() => {
db.run("BEGIN TRANSACTION");
const tables = ['teams', 'challenges', 'solves', 'blogs', 'config'];
tables.forEach(table => {
db.run(`DELETE FROM ${table}`);
if (!data[table] || data[table].length === 0) return;
const rows = data[table];
const cols = Object.keys(rows[0]);
const stmt = db.prepare(`INSERT INTO ${table} (${cols.join(',')}) VALUES (${cols.map(()=>'?').join(',')})`);
rows.forEach(row => {
if (table === 'challenges') {
const files = JSON.parse(row.files || '[]');
row.files = JSON.stringify(files.map(file => {
if (file.base64) {
fs.writeFileSync(path.join(uploadDir, path.basename(file.url)), Buffer.from(file.base64, 'base64'));
const { base64, ...rest } = file;
return rest;
}
return file;
}));
}
stmt.run(Object.values(row));
});
stmt.finalize();
});
db.run("COMMIT", (err) => { if (err) return res.status(500).json({ success: false }); res.json({ success: true }); });
});
} catch (e) { res.status(500).json({ message: e.message }); }
});
apiRouter.post('/challenges/submit', (req, res) => {
const authHeader = req.headers.authorization;
const teamId = authHeader ? authHeader.replace('Bearer mock-token-', '') : null;
const { challengeId, flag } = req.body;
if (!teamId) return res.status(401).json({ success: false });
db.get("SELECT isDisabled 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()], (err) => {
res.json({ success: true });
});
} else {
res.json({ success: false });
}
db.all("SELECT key, value FROM config", (err, configRows) => {
const config = {};
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) => {
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 }));
} else res.json({ success: false });
});
});
});
});
// Admin routes
apiRouter.delete('/admin/challenges/all', (req, res) => {
db.all("SELECT files FROM challenges", (err, rows) => {
rows.forEach(r => unlinkFiles(r.files));
db.serialize(() => { db.run("DELETE FROM challenges"); db.run("DELETE FROM solves", () => res.json({ success: true })); });
});
});
apiRouter.post('/admin/toggle-ctf', (req, res) => {
db.get("SELECT value FROM config WHERE key = 'isStarted'", (err, row) => {
const newValue = row?.value === 'true' ? 'false' : 'true';
db.run("UPDATE config SET value = ? WHERE key = 'isStarted'", [newValue], () => {
res.json({ success: true, isStarted: newValue === 'true' });
});
db.run("UPDATE config SET value = ? WHERE key = 'isStarted'", [newValue], () => res.json({ success: true, isStarted: newValue === 'true' }));
});
});
apiRouter.put('/profile', (req, res) => {
const authHeader = req.headers.authorization;
const teamId = authHeader ? authHeader.replace('Bearer mock-token-', '') : null;
const { password } = req.body;
if (!teamId) return res.status(401).json({ message: 'Unauthorized' });
if (!password) return res.status(400).json({ message: 'Password required' });
apiRouter.post('/admin/reset-scores', (req, res) => { db.run("DELETE FROM solves", () => res.json({ success: true })); });
db.run("UPDATE teams SET password = ? WHERE id = ?", [password, teamId], function(err) {
if (err) return res.status(500).json({ message: 'Update failed' });
res.json({ success: true });
});
apiRouter.put('/profile', (req, res) => {
const teamId = req.headers.authorization?.replace('Bearer mock-token-', '');
if (!teamId) return res.status(401).json({ message: 'Unauthorized' });
db.run("UPDATE teams SET password = ? WHERE id = ?", [hashPassword(req.body.password), teamId], () => res.json({ success: true }));
});
apiRouter.put('/admin/teams/:id', (req, res) => {
const { name, isDisabled, password, isAdmin } = req.body;
const id = req.params.id;
// Protect root admin from demotion or disabling
const finalIsDisabled = id === 'admin-0' ? 0 : (isDisabled ? 1 : 0);
const finalIsAdmin = id === 'admin-0' ? 1 : (isAdmin ? 1 : 0);
const { name, isDisabled, password, isAdmin } = req.body, id = req.params.id;
let query = "UPDATE teams SET name = ?, isDisabled = ?, isAdmin = ?";
let params = [name, finalIsDisabled, finalIsAdmin];
if (password) {
query += ", password = ?";
params.push(password);
}
query += " WHERE id = ?";
params.push(id);
db.run(query, params, function(err) {
if (err) return res.status(400).json({ message: 'Update failed.' });
res.json({ success: true });
});
let params = [name, id === 'admin-0' ? 0 : (isDisabled ? 1 : 0), id === 'admin-0' ? 1 : (isAdmin ? 1 : 0)];
if (password) { query += ", password = ?"; params.push(hashPassword(password)); }
query += " WHERE id = ?"; params.push(id);
db.run(query, params, () => res.json({ success: true }));
});
apiRouter.delete('/admin/teams/:id', (req, res) => {
const id = req.params.id;
if (id === 'admin-0') return res.status(403).json({ message: 'Cannot delete root admin' });
db.run("DELETE FROM teams WHERE id = ?", [id], () => {
db.run("DELETE FROM solves WHERE teamId = ?", [id], () => {
res.json({ success: true });
});
});
if (req.params.id === 'admin-0') return res.status(403).json({ message: 'Protected' });
db.run("DELETE FROM teams WHERE id = ?", [req.params.id], () => db.run("DELETE FROM solves WHERE teamId = ?", [req.params.id], () => res.json({ success: true })));
});
// Blog Management
apiRouter.post('/admin/blogs', (req, res) => {
const { title, content } = req.body;
if (!title || !content) return res.status(400).json({ message: 'Title and content required' });
const id = 'blog-' + Math.random().toString(36).substr(2, 9);
const timestamp = Date.now();
db.run("INSERT INTO blogs (id, title, content, timestamp) VALUES (?, ?, ?, ?)",
[id, title, content, timestamp], (err) => {
if (err) return res.status(500).json({ message: err.message });
res.json({ success: true, id });
}
);
db.run("INSERT INTO blogs (id, title, content, timestamp) VALUES (?, ?, ?, ?)", [id, req.body.title, req.body.content, Date.now()], () => res.json({ success: true, id }));
});
apiRouter.put('/admin/blogs/:id', (req, res) => {
const { title, content } = req.body;
const { id } = req.params;
if (!title || !content) return res.status(400).json({ message: 'Title and content required' });
apiRouter.put('/admin/blogs/:id', (req, res) => { db.run("UPDATE blogs SET title = ?, content = ? WHERE id = ?", [req.body.title, req.body.content, req.params.id], () => res.json({ success: true })); });
apiRouter.delete('/admin/blogs/:id', (req, res) => { db.run("DELETE FROM blogs WHERE id = ?", [req.params.id], () => res.json({ success: true })); });
db.run("UPDATE blogs SET title = ?, content = ? WHERE id = ?",
[title, content, id], (err) => {
if (err) return res.status(500).json({ message: err.message });
res.json({ success: true });
}
);
});
apiRouter.delete('/admin/blogs/:id', (req, res) => {
db.run("DELETE FROM blogs WHERE id = ?", [req.params.id], (err) => {
if (err) return res.status(500).json({ message: err.message });
res.json({ success: true });
});
});
// Challenges Management
apiRouter.post('/admin/challenges', upload.array('files'), (req, res) => {
const { title, category, difficulty, description, initialPoints, flag, port, connectionType } = req.body;
if (!title || !flag) return res.status(400).json({ message: 'Title and flag are required.' });
const { title, category, difficulty, description, initialPoints, minimumPoints, decaySolves, flag, port, connectionType, overrideIp } = req.body;
const id = 'chal-' + Math.random().toString(36).substr(2, 9);
const files = (req.files || []).map(f => ({
name: f.originalname,
url: `/files/${f.filename}`
}));
db.run(`INSERT INTO challenges (id, title, category, difficulty, description, initialPoints, flag, files, port, connectionType)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[id, title, category, difficulty, description, parseInt(initialPoints) || 0, flag, JSON.stringify(files), parseInt(port) || 0, connectionType || 'nc'],
(err) => {
if (err) {
console.error("Error creating challenge:", err.message);
return res.status(500).json({ message: err.message });
}
res.json({ id, success: true });
}
const files = (req.files || []).map(f => ({ name: f.originalname, url: `/files/${f.filename}` }));
db.run(`INSERT INTO challenges (id, title, category, difficulty, description, initialPoints, minimumPoints, decaySolves, flag, files, port, connectionType, overrideIp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[id, title, category, difficulty, description, parseInt(initialPoints) || 0, parseInt(minimumPoints) || 0, parseInt(decaySolves) || 1, flag, JSON.stringify(files), parseInt(port) || 0, connectionType || 'nc', overrideIp],
() => res.json({ id, success: true })
);
});
apiRouter.put('/admin/challenges/:id', upload.array('files'), (req, res) => {
const { title, category, difficulty, description, initialPoints, flag, existingFiles, port, connectionType } = req.body;
const id = req.params.id;
if (!id) return res.status(400).json({ message: 'Challenge ID is required for update.' });
let files = [];
try {
files = JSON.parse(existingFiles || '[]');
} catch (e) {
console.error("Error parsing existingFiles:", e);
files = [];
}
if (req.files) {
req.files.forEach(f => {
files.push({
name: f.originalname,
url: `/files/${f.filename}`
});
});
}
const query = `UPDATE challenges SET title=?, category=?, difficulty=?, description=?, initialPoints=?, flag=?, files=?, port=?, connectionType=? WHERE id=?`;
const params = [
title || "",
category || "WEB",
difficulty || "Low",
description || "",
parseInt(initialPoints) || 0,
flag || "",
JSON.stringify(files),
port ? parseInt(port) : 0,
connectionType || 'nc',
id
];
db.run(query, params, function(err) {
if (err) {
console.error("Error updating challenge:", err.message);
return res.status(500).json({ message: err.message });
}
if (this.changes === 0) {
console.warn(`No challenge found with ID: ${id}`);
return res.status(404).json({ message: "Challenge not found." });
}
res.json({ success: true, id });
db.get("SELECT files FROM challenges WHERE id = ?", [id], (err, row) => {
let files = JSON.parse(req.body.existingFiles || '[]'), oldFiles = JSON.parse(row.files || '[]');
oldFiles.forEach(of => { if (!files.find(f => f.url === of.url)) { const filePath = path.join(uploadDir, path.basename(of.url)); if (fs.existsSync(filePath)) fs.unlinkSync(filePath); } });
if (req.files) req.files.forEach(f => files.push({ name: f.originalname, url: `/files/${f.filename}` }));
const query = `UPDATE challenges SET title=?, category=?, difficulty=?, description=?, initialPoints=?, minimumPoints=?, decaySolves=?, flag=?, files=?, port=?, connectionType=?, overrideIp=? WHERE id=?`;
const params = [req.body.title, req.body.category, req.body.difficulty, req.body.description, parseInt(req.body.initialPoints), parseInt(req.body.minimumPoints), parseInt(req.body.decaySolves), req.body.flag, JSON.stringify(files), parseInt(req.body.port), req.body.connectionType, req.body.overrideIp, id];
db.run(query, params, () => res.json({ success: true, id }));
});
});
apiRouter.delete('/admin/challenges/:id', (req, res) => {
db.run("DELETE FROM challenges WHERE id = ?", [req.params.id], (err) => {
if (err) return res.status(500).json({ message: err.message });
db.run("DELETE FROM solves WHERE challengeId = ?", [req.params.id], () => {
res.json({ success: true });
});
db.get("SELECT files FROM challenges WHERE id = ?", [req.params.id], (err, row) => {
if (row) unlinkFiles(row.files);
db.serialize(() => { db.run("DELETE FROM challenges WHERE id = ?", [req.params.id]); db.run("DELETE FROM solves WHERE challengeId = ?", [req.params.id], () => res.json({ success: true })); });
});
});
app.use('/api', apiRouter);
const distPath = path.join(__dirname, 'dist');
if (fs.existsSync(distPath)) {
app.use(express.static(distPath));
app.get('*', (req, res) => {
if (!req.path.startsWith('/api') && !req.path.startsWith('/files')) {
res.sendFile(path.join(distPath, 'index.html'));
} else {
res.status(404).json({ error: 'API resource not found' });
}
});
app.get('*', (req, res) => { if (!req.path.startsWith('/api') && !req.path.startsWith('/files')) res.sendFile(path.join(distPath, 'index.html')); else res.status(404).json({ error: 'Not found' }); });
}
app.listen(port, '0.0.0.0', () => {
console.log(`HIP6 CTF Backend Server running at http://127.0.0.1:${port}`);
});
app.listen(port, '0.0.0.0', () => console.log(`CTF Backend running on port ${port}`));