feat: polish gameplay and admin flow
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user