initial commit

This commit is contained in:
m0rph3us1987
2026-01-07 13:27:11 +01:00
commit 1c756af238
19 changed files with 6603 additions and 0 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
node_modules
dist
# Exclude these so they are not baked into the image
uploads
ctf.db
.git
.DS_Store

28
.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Local files
uploads/*
ctf.db

1249
App.tsx Normal file

File diff suppressed because it is too large Load Diff

44
Dockerfile Normal file
View File

@@ -0,0 +1,44 @@
# --- Stage 1: Build Frontend ---
FROM node:20-alpine AS builder
WORKDIR /app
# Copy dependency files
COPY package*.json ./
RUN npm ci
# Copy source code (files in .dockerignore are excluded)
COPY . .
# Build the frontend
RUN npm run build
# --- Stage 2: Production Server ---
FROM node:20-alpine
WORKDIR /app
# Install dependencies for sqlite3
RUN apk add --no-cache python3 make g++
# Copy package files and install production dependencies
COPY package*.json ./
RUN npm ci --omit=dev
# Copy backend server and config
COPY server.js .
COPY metadata.json .
# Copy built frontend from builder stage
COPY --from=builder /app/dist ./dist
# 1. Create the uploads folder explicitly
# 2. Set ownership of the app directory to the 'node' user
RUN mkdir -p uploads && chown -R node:node /app
# Switch to non-root user
USER node
EXPOSE 3000
CMD ["node", "server.js"]

20
README.md Normal file
View File

@@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/drive/1hEzZiQvXvEesEyPd55AgRScM8zJxJvsT
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

51
constants.ts Normal file
View File

@@ -0,0 +1,51 @@
import { Challenge, Team } from './types';
export const CATEGORIES = ['WEB', 'PWN', 'REV', 'CRY', 'MSC'];
export const DIFFICULTIES: ('Low' | 'Medium' | 'High')[] = ['Low', 'Medium', 'High'];
export const INITIAL_CHALLENGES: Challenge[] = [
{
id: '1',
title: 'algorave',
category: 'REV',
difficulty: 'Low',
description: 'The sound of the future is encrypted. Can you find the melody?',
initialPoints: 100,
flag: 'CTF{view_source_is_key}',
// Fix: Added missing 'files' property
files: [],
solves: []
},
{
id: '2',
title: 'shell(de)coding',
category: 'MSC',
difficulty: 'Medium',
description: 'Wait, this shellcode looks like a poem...',
initialPoints: 300,
flag: 'CTF{xor_is_not_encryption}',
// Fix: Added missing 'files' property
files: [],
solves: []
},
{
id: '3',
title: 'worrier',
category: 'CRY',
difficulty: 'High',
description: 'Anxious math leads to anxious flags.',
initialPoints: 500,
flag: 'CTF{worrier_not_warrior}',
// Fix: Added missing 'files' property
files: [],
solves: []
}
];
export const ADMIN_TEAM: Team = {
id: 'admin-0',
name: 'admin',
password: 'admin',
isAdmin: true
};

9
docker-compose.yml Normal file
View File

@@ -0,0 +1,9 @@
services:
ctf-app:
build: .
container_name: hipctf
ports:
- "3000:3000"
environment:
- NODE_ENV=production
restart: unless-stopped

64
index.html Normal file
View File

@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>hipCTF26</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700;800&display=swap" rel="stylesheet">
<style>
body {
font-family: 'JetBrains Mono', monospace;
background-color: #000000;
color: #ff0000;
}
.hxp-border {
border: 3px solid #ff0000;
}
.hxp-border-purple {
border: 2px solid #bf00ff;
}
.hxp-btn {
background: #ff0000;
color: #000;
font-weight: 800;
padding: 10px 20px;
transition: all 0.1s;
}
.hxp-btn:hover {
background: #000;
color: #ff0000;
box-shadow: 0 0 15px #ff0000;
}
.hxp-card {
background: #000;
border: 2px solid #333;
}
.hxp-card:hover {
border-color: #bf00ff;
}
::-webkit-scrollbar { width: 10px; }
::-webkit-scrollbar-track { background: #000; }
::-webkit-scrollbar-thumb { background: #ff0000; }
</style>
<script type="importmap">
{
"imports": {
"react-dom/": "https://esm.sh/react-dom@^19.2.3/",
"react/": "https://esm.sh/react@^19.2.3/",
"react": "https://esm.sh/react@^19.2.3",
"react-router-dom": "https://esm.sh/react-router-dom@^7.11.0",
"lucide-react": "https://esm.sh/lucide-react@^0.562.0",
"vite": "https://esm.sh/vite@^7.3.0",
"@vitejs/plugin-react": "https://esm.sh/@vitejs/plugin-react@^5.1.2"
}
}
</script>
<link rel="stylesheet" href="/index.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html>

16
index.tsx Normal file
View File

@@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error("Could not find root element to mount to");
}
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

5
metadata.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "HIPCTF Platform",
"description": "A comprehensive Capture The Flag platform build for the Hack Im Pott",
"requestFramePermissions": []
}

4329
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "hipctf",
"version": "1.0.0",
"main": "server.js",
"dependencies": {
"express": "^4.18.2",
"sqlite3": "^5.1.6",
"multer": "^1.4.5-lts.1",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.0",
"lucide-react": "^0.474.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.0"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"start": "node server.js"
}
}

405
server.js Normal file
View File

@@ -0,0 +1,405 @@
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}`);
});

179
services/api.ts Normal file
View File

@@ -0,0 +1,179 @@
import { Challenge, Team, Solve, CTFState, BlogPost } from '../types';
const API_BASE = '/api';
class ApiService {
private getHeaders() {
const session = localStorage.getItem('hip6_session');
const headers: HeadersInit = {
'Content-Type': 'application/json',
};
if (session) {
const { token } = JSON.parse(session);
if (token) headers['Authorization'] = `Bearer ${token}`;
}
return headers;
}
async getState(): Promise<CTFState> {
try {
const res = await fetch(`${API_BASE}/state`);
if (!res.ok) throw new Error(`HTTP Error ${res.status}`);
return res.json();
} catch (err) {
console.error('API Error (getState):', err);
throw err;
}
}
async login(name: string, pass: string): Promise<{ team: Team, token: string }> {
const res = await fetch(`${API_BASE}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, password: pass }),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.message || 'Invalid credentials');
}
return res.json();
}
async register(name: string, pass: string): Promise<{ team: Team, token: string }> {
const res = await fetch(`${API_BASE}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, password: pass }),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.message || 'Registration failed');
}
return res.json();
}
async toggleCtf(): Promise<void> {
const res = await fetch(`${API_BASE}/admin/toggle-ctf`, {
method: 'POST',
headers: this.getHeaders(),
});
if (!res.ok) throw new Error('Unauthorized or failed to toggle CTF');
}
async updateConfig(formData: FormData): Promise<void> {
const session = localStorage.getItem('hip6_session');
const headers: HeadersInit = {};
if (session) {
const { token } = JSON.parse(session);
headers['Authorization'] = `Bearer ${token}`;
}
const res = await fetch(`${API_BASE}/admin/config`, {
method: 'PUT',
headers,
body: formData,
});
if (!res.ok) throw new Error('Failed to update configuration');
}
async updateTeam(id: string, data: { name: string, isDisabled: boolean, isAdmin: boolean, password?: string }): Promise<void> {
const res = await fetch(`${API_BASE}/admin/teams/${id}`, {
method: 'PUT',
headers: this.getHeaders(),
body: JSON.stringify(data),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.message || 'Failed to update team');
}
}
async updateProfile(data: { password?: string }): Promise<void> {
const res = await fetch(`${API_BASE}/profile`, {
method: 'PUT',
headers: this.getHeaders(),
body: JSON.stringify(data),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.message || 'Failed to update profile');
}
}
async deleteTeam(id: string): Promise<void> {
const res = await fetch(`${API_BASE}/admin/teams/${id}`, {
method: 'DELETE',
headers: this.getHeaders(),
});
if (!res.ok) throw new Error('Failed to delete team');
}
async submitFlag(challengeId: string, flag: string): Promise<{ success: boolean }> {
const res = await fetch(`${API_BASE}/challenges/submit`, {
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify({ challengeId, flag }),
});
if (!res.ok) throw new Error('Submission failed');
return res.json();
}
async upsertChallenge(formData: FormData, id?: string): Promise<any> {
const method = id ? 'PUT' : 'POST';
const url = id ? `${API_BASE}/admin/challenges/${id}` : `${API_BASE}/admin/challenges`;
const session = localStorage.getItem('hip6_session');
const headers: HeadersInit = {};
if (session) {
const { token } = JSON.parse(session);
headers['Authorization'] = `Bearer ${token}`;
}
const res = await fetch(url, {
method,
headers,
body: formData,
});
if (!res.ok) {
const err = await res.json().catch(() => ({ message: 'Save failed' }));
throw new Error(err.message || 'Failed to save challenge');
}
return res.json();
}
async deleteChallenge(id: string): Promise<void> {
const res = await fetch(`${API_BASE}/admin/challenges/${id}`, {
method: 'DELETE',
headers: this.getHeaders(),
});
if (!res.ok) throw new Error('Failed to delete challenge');
}
async createBlogPost(data: { title: string, content: string }): Promise<void> {
const res = await fetch(`${API_BASE}/admin/blogs`, {
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify(data),
});
if (!res.ok) throw new Error('Failed to create blog post');
}
async updateBlogPost(id: string, data: { title: string, content: string }): Promise<void> {
const res = await fetch(`${API_BASE}/admin/blogs/${id}`, {
method: 'PUT',
headers: this.getHeaders(),
body: JSON.stringify(data),
});
if (!res.ok) throw new Error('Failed to update blog post');
}
async deleteBlogPost(id: string): Promise<void> {
const res = await fetch(`${API_BASE}/admin/blogs/${id}`, {
method: 'DELETE',
headers: this.getHeaders(),
});
if (!res.ok) throw new Error('Failed to delete blog post');
}
}
export const api = new ApiService();

3
services/database.ts Normal file
View File

@@ -0,0 +1,3 @@
// Obsolete: Replaced by server-side SQLite3 in server.js
export const localBackend = {};

58
services/scoring.ts Normal file
View File

@@ -0,0 +1,58 @@
/**
* Dynamic Scoring Algorithm
* Logic:
* 1. Base Score starts at initialPoints.
* 2. As solves increase, the score for EVERYONE who solved it decreases.
* 3. We use a decay function: currentPoints = max(min_points, initial * decay_factor ^ (solves - 1))
* 4. Plus a "First Blood" bonus: 1st solver gets 10% extra, 2nd 5%, 3rd 2%.
*/
const MIN_POINTS_PERCENTAGE = 0.2; // Points won't drop below 20% of initial
const DECAY_CONSTANT = 0.92; // Aggressive decay per solve
export const calculateChallengeValue = (initialPoints: number, solveCount: number): number => {
if (solveCount === 0) return initialPoints;
const minPoints = Math.floor(initialPoints * MIN_POINTS_PERCENTAGE);
const decayedPoints = Math.floor(initialPoints * Math.pow(DECAY_CONSTANT, solveCount - 1));
return Math.max(minPoints, decayedPoints);
};
export const getFirstBloodBonus = (rank: number): number => {
if (rank === 0) return 0.10; // 1st
if (rank === 1) return 0.05; // 2nd
if (rank === 2) return 0.02; // 3rd
return 0;
};
export const calculateTeamTotalScore = (
teamId: string,
challenges: { id: string, initialPoints: number, solves: string[] }[],
solves: { teamId: string, challengeId: string, timestamp: number }[]
): number => {
let total = 0;
// Filter solves for this team
const teamSolves = solves.filter(s => s.teamId === teamId);
teamSolves.forEach(solve => {
const challenge = challenges.find(c => c.id === solve.challengeId);
if (!challenge) return;
// Current value of challenge (shared by all)
const baseValue = calculateChallengeValue(challenge.initialPoints, challenge.solves.length);
// Calculate rank for bonus
// Find all solves for this challenge sorted by time
const challengeSolves = solves
.filter(s => s.challengeId === solve.challengeId)
.sort((a, b) => a.timestamp - b.timestamp);
const rank = challengeSolves.findIndex(s => s.teamId === teamId);
const bonus = Math.floor(baseValue * getFirstBloodBonus(rank));
total += (baseValue + bonus);
});
return total;
};

29
tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"types": [
"node"
],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

53
types.ts Normal file
View File

@@ -0,0 +1,53 @@
export type Difficulty = 'Low' | 'Medium' | 'High';
export interface ChallengeFile {
name: string;
url: string;
}
export interface Challenge {
id: string;
title: string;
category: string;
difficulty: Difficulty;
description: string;
initialPoints: number;
flag: string;
files: ChallengeFile[];
solves: string[]; // Team IDs
port?: number;
connectionType?: 'nc' | 'http';
}
export interface Team {
id: string;
name: string;
password?: string;
isAdmin?: boolean | number;
isDisabled?: boolean | number;
}
export interface Solve {
teamId: string;
challengeId: string;
timestamp: number;
pointsEarned: number; // Stored at time of solve, but usually recalculated dynamically
}
export interface BlogPost {
id: string;
title: string;
content: string;
timestamp: number;
}
export interface CTFState {
isStarted: boolean;
startTime: number | null;
teams: Team[];
challenges: Challenge[];
solves: Solve[];
blogs: BlogPost[];
config: Record<string, string>;
}

26
vite.config.ts Normal file
View File

@@ -0,0 +1,26 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
// Directs API calls from Vite (port 5173) to Express (port 3000)
// Using 127.0.0.1 is more stable than 'localhost' in some environments
'/api': {
target: 'http://127.0.0.1:3000',
changeOrigin: true,
secure: false,
},
// Directs file download calls to the uploads folder served by Express
'/files': {
target: 'http://127.0.0.1:3000',
changeOrigin: true,
secure: false,
}
}
}
});