feat: polish gameplay and admin flow

This commit is contained in:
Jeremy Brandenburger
2026-04-03 11:59:24 +02:00
parent c8003cc77c
commit f5f57c3e4d
72 changed files with 2286 additions and 502 deletions
+105 -1
View File
@@ -311,7 +311,22 @@ app.post('/api/change-password', requireAuth, async (req, res) => {
// ── User management (admin) ───────────────────────────────────────────────────
app.get('/api/users', requireAuth, requireAdmin, (req, res) => {
const users = db.prepare('SELECT username, role, mustChange FROM users ORDER BY username').all();
const users = db.prepare(`
SELECT
u.username,
u.role,
u.mustChange,
COALESCE(u.elo, 1000) AS elo,
COALESCE(ps.sessions, 0) AS sessions,
ps.lastPlayed AS lastPlayed
FROM users u
LEFT JOIN (
SELECT username, COUNT(*) AS sessions, MAX(created_at) AS lastPlayed
FROM practice_sessions
GROUP BY username
) ps ON ps.username = u.username
ORDER BY u.username
`).all();
res.json(users.map(u => ({ ...u, mustChange: u.mustChange === 1 })));
});
@@ -347,6 +362,95 @@ app.delete('/api/users/:username', requireAuth, requireAdmin, (req, res) => {
res.json({ ok: true });
});
app.post('/api/users/:username/reset-password', requireAuth, requireAdmin, async (req, res) => {
const { username } = req.params;
const user = db.prepare('SELECT username FROM users WHERE username = ?').get(username);
if (!user) return res.status(404).json({ error: 'User not found' });
const tempPw = crypto.randomBytes(6).toString('hex');
const hash = await bcrypt.hash(tempPw, 12);
db.prepare('UPDATE users SET hash = ?, mustChange = 1 WHERE username = ?').run(hash, username);
res.json({ ok: true, tempPassword: tempPw });
});
app.patch('/api/users/:username', requireAuth, requireAdmin, (req, res) => {
const { username } = req.params;
const { role } = req.body || {};
if (!['admin', 'user'].includes(role)) return res.status(400).json({ error: 'Valid role required' });
const user = db.prepare('SELECT username, role FROM users WHERE username = ?').get(username);
if (!user) return res.status(404).json({ error: 'User not found' });
if (username === req.session.user && role !== 'admin') {
return res.status(400).json({ error: 'You cannot demote yourself' });
}
if (user.role === 'admin' && role !== 'admin') {
const adminCount = db.prepare("SELECT COUNT(*) AS c FROM users WHERE role = 'admin'").get().c;
if (adminCount <= 1) return res.status(400).json({ error: 'Cannot demote the last admin' });
}
db.prepare('UPDATE users SET role = ? WHERE username = ?').run(role, username);
res.json({ ok: true });
});
app.get('/api/admin/overview', requireAuth, requireAdmin, (req, res) => {
const totals = db.prepare(`
SELECT
COUNT(*) AS users,
SUM(CASE WHEN role = 'admin' THEN 1 ELSE 0 END) AS admins,
SUM(CASE WHEN mustChange = 1 THEN 1 ELSE 0 END) AS tempPasswords
FROM users
`).get();
const activity = db.prepare(`
SELECT
(SELECT COUNT(*) FROM practice_sessions) AS practiceSessions,
(SELECT COUNT(*) FROM matches) AS matches,
(SELECT COALESCE(SUM(score), 0) FROM practice_sessions) AS totalPracticeScore
`).get();
const topUser = db.prepare(`
SELECT username, COUNT(*) AS sessions, COALESCE(SUM(score), 0) AS totalScore
FROM practice_sessions
GROUP BY username
ORDER BY totalScore DESC, sessions DESC
LIMIT 1
`).get();
const latestActivity = db.prepare(`
SELECT username, stratagem, score, created_at
FROM practice_sessions
ORDER BY created_at DESC
LIMIT 1
`).get();
res.json({
totals,
activity,
topUser: topUser || null,
latestActivity: latestActivity || null,
});
});
app.get('/api/admin/activity', requireAuth, requireAdmin, (req, res) => {
const practice = db.prepare(`
SELECT username, stratagem, score, mode, created_at
FROM practice_sessions
ORDER BY created_at DESC
LIMIT 10
`).all();
const matches = db.prepare(`
SELECT winner, loser, winner_rounds, loser_rounds, created_at
FROM matches
ORDER BY created_at DESC
LIMIT 10
`).all();
res.json({ practice, matches });
});
// ── Leaderboard cache ─────────────────────────────────────────────────────────
let lbCache = null;
let lbCacheTime = 0;