const express = require('express'); const sqlite3 = require('sqlite3').verbose(); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const cors = require('cors'); const app = express(); const port = process.env.PORT || 3000; // Initialize Database const db = new sqlite3.Database('./ctf.db', (err) => { if (err) console.error('Database connection error:', err.message); else console.log('Connected to the ctf.db SQLite database.'); }); // Setup Storage const uploadDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir); const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, uploadDir), filename: (req, file, cb) => cb(null, Date.now() + '-' + file.originalname) }); 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, password TEXT, isAdmin INTEGER DEFAULT 0, isDisabled INTEGER DEFAULT 0 )`); db.run(`CREATE TABLE IF NOT EXISTS challenges ( id TEXT PRIMARY KEY, title TEXT, category TEXT, difficulty TEXT, description TEXT, initialPoints INTEGER, flag TEXT, files TEXT DEFAULT '[]', port INTEGER, connectionType 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'"); } }); db.run(`CREATE TABLE IF NOT EXISTS solves ( teamId TEXT, challengeId TEXT, timestamp INTEGER, PRIMARY KEY (teamId, challengeId) )`); db.run(`CREATE TABLE IF NOT EXISTS blogs ( id TEXT PRIMARY KEY, title TEXT, content TEXT, timestamp INTEGER )`); db.run(`CREATE TABLE IF NOT EXISTS config ( key TEXT PRIMARY KEY, 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')`); db.run(`INSERT OR IGNORE INTO teams (id, name, password, isAdmin, isDisabled) VALUES ('admin-0', 'admin', 'admin', 1, 0)`); }); // Middleware app.use(cors()); app.use(express.json()); app.use('/files', express.static(uploadDir)); // API Router const apiRouter = express.Router(); // State endpoint 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; }); 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 => ({ ...c, files: JSON.parse(c.files || '[]'), solves: state.solves.filter(s => s.challengeId === c.id).map(s => s.teamId) })); res.json(state); }); }); }); }); }); }); // 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}`; } db.serialize(() => { const stmt = db.prepare("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)"); Object.entries(updates).forEach(([key, value]) => { stmt.run(key, value); }); stmt.finalize(() => { res.json({ success: true }); }); }); }); // Challenges Submit 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 }); } }); }); }); // Admin routes 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' }); }); }); }); 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' }); 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('/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); 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 }); }); }); 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 }); }); }); }); // 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 }); } ); }); 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' }); 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 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 }); } ); }); 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 }); }); }); 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 }); }); }); }); 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.listen(port, '0.0.0.0', () => { console.log(`HIP6 CTF Backend Server running at http://127.0.0.1:${port}`); });