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

View File

@@ -6,9 +6,7 @@ const API_BASE = '/api';
class ApiService {
private getHeaders() {
const session = localStorage.getItem('hip6_session');
const headers: HeadersInit = {
'Content-Type': 'application/json',
};
const headers: HeadersInit = { 'Content-Type': 'application/json' };
if (session) {
const { token } = JSON.parse(session);
if (token) headers['Authorization'] = `Bearer ${token}`;
@@ -17,14 +15,9 @@ class ApiService {
}
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;
}
const res = await fetch(`${API_BASE}/state`);
if (!res.ok) throw new Error(`HTTP Error ${res.status}`);
return res.json();
}
async login(name: string, pass: string): Promise<{ team: Team, token: string }> {
@@ -58,46 +51,84 @@ class ApiService {
method: 'POST',
headers: this.getHeaders(),
});
if (!res.ok) throw new Error('Unauthorized or failed to toggle CTF');
if (!res.ok) throw new Error('Failed to toggle CTF');
}
async resetScores(): Promise<void> {
const res = await fetch(`${API_BASE}/admin/reset-scores`, {
method: 'POST',
headers: this.getHeaders(),
});
if (!res.ok) throw new Error('Failed to reset scores');
}
async backupDatabase(): Promise<any> {
const res = await fetch(`${API_BASE}/admin/db/export`, {
method: 'GET',
headers: this.getHeaders(),
});
if (!res.ok) throw new Error('Backup failed');
return res.json();
}
async restoreDatabase(file: File): Promise<void> {
const formData = new FormData();
formData.append('restoreFile', file);
const session = localStorage.getItem('hip6_session');
const headers: HeadersInit = session ? { 'Authorization': `Bearer ${JSON.parse(session).token}` } : {};
const res = await fetch(`${API_BASE}/admin/db/restore`, { method: 'POST', headers, body: formData });
if (!res.ok) throw new Error('Restore failed');
}
async exportChallenges(): Promise<{ challenges: any[] }> {
const res = await fetch(`${API_BASE}/admin/challenges/export`, {
method: 'GET',
headers: this.getHeaders(),
});
if (!res.ok) throw new Error('Export failed');
return res.json();
}
async importChallenges(file: File): Promise<void> {
const formData = new FormData();
formData.append('importFile', file);
const session = localStorage.getItem('hip6_session');
const headers: HeadersInit = session ? { 'Authorization': `Bearer ${JSON.parse(session).token}` } : {};
const res = await fetch(`${API_BASE}/admin/challenges/import`, { method: 'POST', headers, body: formData });
if (!res.ok) throw new Error('Import failed');
}
async deleteAllChallenges(): Promise<void> {
const res = await fetch(`${API_BASE}/admin/challenges/all`, {
method: 'DELETE',
headers: this.getHeaders(),
});
if (!res.ok) throw new Error('Failed to delete all challenges');
}
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');
const headers: HeadersInit = session ? { 'Authorization': `Bearer ${JSON.parse(session).token}` } : {};
const res = await fetch(`${API_BASE}/admin/config`, { method: 'PUT', headers, body: formData });
if (!res.ok) throw new Error('Failed to update config');
}
async updateTeam(id: string, data: { name: string, isDisabled: boolean, isAdmin: boolean, password?: string }): Promise<void> {
async updateTeam(id: string, data: any): 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');
}
if (!res.ok) throw new Error('Failed to update team');
}
async updateProfile(data: { password?: string }): Promise<void> {
async updateProfile(data: any): 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');
}
if (!res.ok) throw new Error('Failed to update profile');
}
async deleteTeam(id: string): Promise<void> {
@@ -114,30 +145,15 @@ class ApiService {
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');
}
const headers: HeadersInit = session ? { 'Authorization': `Bearer ${JSON.parse(session).token}` } : {};
const res = await fetch(url, { method, headers, body: formData });
return res.json();
}
@@ -149,22 +165,20 @@ class ApiService {
if (!res.ok) throw new Error('Failed to delete challenge');
}
async createBlogPost(data: { title: string, content: string }): Promise<void> {
async createBlogPost(data: any): 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> {
async updateBlogPost(id: string, data: any): 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> {
@@ -172,7 +186,6 @@ class ApiService {
method: 'DELETE',
headers: this.getHeaders(),
});
if (!res.ok) throw new Error('Failed to delete blog post');
}
}

View File

@@ -1,33 +1,37 @@
/**
* 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%.
* Dynamic Scoring Algorithm (Parabolic Decay)
* Logic:
* 1. Base Value = ((minimum - initial) / (decay^2)) * (solve_count^2) + initial
* 2. If solve_count >= decay, return minimum.
* 3. Fixed Bonuses (based on initial points): 1st: 15%, 2nd: 10%, 3rd: 5%.
*/
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 = (
initial: number,
minimum: number,
decay: number,
solveCount: number
): number => {
if (solveCount === 0) return initial;
if (decay <= 0 || solveCount >= decay) return minimum;
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);
// Parabolic formula: ((min - init) / decay^2) * solveCount^2 + init
const value = ((minimum - initial) / (decay * decay)) * (solveCount * solveCount) + initial;
return Math.ceil(value);
};
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
export const getFirstBloodBonusFactor = (rank: number): number => {
if (rank === 0) return 0.15; // 1st
if (rank === 1) return 0.10; // 2nd
if (rank === 2) return 0.05; // 3rd
return 0;
};
export const calculateTeamTotalScore = (
teamId: string,
challenges: { id: string, initialPoints: number, solves: string[] }[],
challenges: any[],
solves: { teamId: string, challengeId: string, timestamp: number }[]
): number => {
let total = 0;
@@ -39,17 +43,21 @@ export const calculateTeamTotalScore = (
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);
// Current dynamic base value (retroactive for everyone)
const baseValue = calculateChallengeValue(
challenge.initialPoints,
challenge.minimumPoints || 0,
challenge.decaySolves || 1,
challenge.solves.length
);
// Calculate rank for bonus
// Find all solves for this challenge sorted by time
// Calculate rank for fixed bonus based on initial value
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));
const bonus = Math.floor(challenge.initialPoints * getFirstBloodBonusFactor(rank));
total += (baseValue + bonus);
});