leaderboard/server/routes.js

164 lines
5.3 KiB
JavaScript
Raw Permalink Normal View History

const express = require('express');
const db = require('./db');
const { requireAdmin, isValidAdminKey } = require('./auth');
const router = express.Router();
function todayStr() {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
// Resolves a caller-supplied date, falling back to today and rejecting
// malformed or future dates (returns null on invalid input).
function resolveDate(raw) {
if (!raw) return todayStr();
if (!DATE_RE.test(raw)) return null;
if (raw > todayStr()) return null;
return raw;
}
// ---- Players -----------------------------------------------------------
router.get('/players', (req, res) => {
const date = resolveDate(req.query.date) || todayStr();
const rows = db
.prepare(
`SELECT p.id, p.name,
COUNT(d.id) AS total,
SUM(CASE WHEN d.date = ? THEN 1 ELSE 0 END) AS dateCount
FROM players p
LEFT JOIN doses d ON d.player_id = p.id
GROUP BY p.id
ORDER BY total DESC, p.name COLLATE NOCASE ASC`
)
.all(date);
res.json(rows.map((r) => ({ ...r, dateCount: r.dateCount || 0 })));
});
router.post('/players', requireAdmin, (req, res) => {
const name = String(req.body?.name || '').trim();
if (!name) {
return res.status(400).json({ error: 'Le nom est requis' });
}
if (name.length > 40) {
return res.status(400).json({ error: 'Le nom est trop long (40 caractères maximum)' });
}
try {
const info = db.prepare('INSERT INTO players (name) VALUES (?)').run(name);
res.status(201).json({ id: info.lastInsertRowid, name, total: 0, dateCount: 0 });
} catch (err) {
if (err.code === 'ERR_SQLITE_ERROR' && /UNIQUE constraint failed/.test(err.message)) {
return res.status(409).json({ error: 'Un joueur avec ce nom existe déjà' });
}
throw err;
}
});
router.delete('/players/:id', requireAdmin, (req, res) => {
const playerId = Number(req.params.id);
const player = db.prepare('SELECT id FROM players WHERE id = ?').get(playerId);
if (!player) {
return res.status(404).json({ error: 'Joueur introuvable' });
}
db.exec('BEGIN');
db.prepare('DELETE FROM doses WHERE player_id = ?').run(playerId);
db.prepare('DELETE FROM players WHERE id = ?').run(playerId);
db.exec('COMMIT');
res.status(204).end();
});
// ---- Timeseries (doses per player, per date) -----------------------------
router.get('/timeseries', (req, res) => {
const players = db.prepare('SELECT id, name FROM players ORDER BY id ASC').all();
const dates = db
.prepare('SELECT DISTINCT date FROM doses ORDER BY date ASC')
.all()
.map((r) => r.date);
const counts = db
.prepare('SELECT player_id, date, COUNT(*) AS c FROM doses GROUP BY player_id, date')
.all();
const byPlayerDate = new Map(); // `${playerId}|${date}` -> count
for (const row of counts) {
byPlayerDate.set(`${row.player_id}|${row.date}`, row.c);
}
const series = players.map((p) => ({
playerId: p.id,
name: p.name,
counts: dates.map((date) => byPlayerDate.get(`${p.id}|${date}`) || 0),
}));
res.json({ dates, series });
});
// ---- Admin auth ----------------------------------------------------------
router.post('/admin/verify', (req, res) => {
const password = String(req.body?.password || '');
res.json({ ok: isValidAdminKey(password) });
});
// ---- Doses (admin only) --------------------------------------------------
router.post('/doses', requireAdmin, (req, res) => {
const playerId = Number(req.body?.playerId);
const player = db.prepare('SELECT id FROM players WHERE id = ?').get(playerId);
if (!player) {
return res.status(404).json({ error: 'Joueur introuvable' });
}
const date = resolveDate(req.body?.date);
if (!date) {
return res.status(400).json({ error: 'Date invalide (elle ne peut pas être dans le futur)' });
}
db.prepare('INSERT INTO doses (player_id, date) VALUES (?, ?)').run(playerId, date);
const total = db.prepare('SELECT COUNT(*) AS c FROM doses WHERE player_id = ?').get(playerId).c;
const dateCount = db
.prepare('SELECT COUNT(*) AS c FROM doses WHERE player_id = ? AND date = ?')
.get(playerId, date).c;
res.status(201).json({ playerId, total, dateCount });
});
router.delete('/doses/latest', requireAdmin, (req, res) => {
const playerId = Number(req.query.playerId);
const player = db.prepare('SELECT id FROM players WHERE id = ?').get(playerId);
if (!player) {
return res.status(404).json({ error: 'Joueur introuvable' });
}
const date = resolveDate(req.query.date);
if (!date) {
return res.status(400).json({ error: 'Date invalide (elle ne peut pas être dans le futur)' });
}
const latest = db
.prepare('SELECT id FROM doses WHERE player_id = ? AND date = ? ORDER BY id DESC LIMIT 1')
.get(playerId, date);
if (!latest) {
return res.status(404).json({ error: 'Aucune dose enregistrée pour ce joueur à cette date' });
}
db.prepare('DELETE FROM doses WHERE id = ?').run(latest.id);
const total = db.prepare('SELECT COUNT(*) AS c FROM doses WHERE player_id = ?').get(playerId).c;
const dateCount = db
.prepare('SELECT COUNT(*) AS c FROM doses WHERE player_id = ? AND date = ?')
.get(playerId, date).c;
res.json({ playerId, total, dateCount });
});
module.exports = router;