initial commit
This commit is contained in:
179
services/api.ts
Normal file
179
services/api.ts
Normal 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
3
services/database.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
// Obsolete: Replaced by server-side SQLite3 in server.js
|
||||
export const localBackend = {};
|
||||
58
services/scoring.ts
Normal file
58
services/scoring.ts
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user