feat(admin): phase 5 — per-row quick actions for users + sessions
Hover-only action buttons (right-aligned, opacity transition, hidden on mobile). - users.js: 4 actions (ban/unban, award coins, sessions, delete) — replaces `>` glyph cell, falls back to glyph for non-admin / self - sessions.js: 2 actions (view, delete) - DELETE /api/admin/sessions/:id (NEW): transactional (assignment_sessions=NULL, user_answers, session_questions, test_sessions), audit-logged, admin-only - event.stopPropagation defence-in-depth (each button + parent .row-actions) - LS.confirm for destructive ops; LS.modal for award-coins amount/reason - CSS injected once via #row-actions-style id-dedup (same content in both sections) Existing user-panel overlay + session toggle-drawer flows untouched (Phase 6 removes overlay). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -293,6 +293,33 @@ function getSessionDetail(req, res) {
|
||||
res.json(session);
|
||||
}
|
||||
|
||||
/* ── DELETE /api/admin/sessions/:id ──────────────────────────────────── */
|
||||
const _deleteSessionTx = db.transaction((sid) => {
|
||||
// assignment_sessions references test_sessions with ON DELETE SET NULL,
|
||||
// but we explicitly null it so the assignment slot stays usable.
|
||||
db.prepare('UPDATE assignment_sessions SET session_id = NULL WHERE session_id = ?').run(sid);
|
||||
// user_answers / session_questions cascade via ON DELETE CASCADE,
|
||||
// but delete explicitly for visibility and to mirror clearUserSessions().
|
||||
db.prepare('DELETE FROM user_answers WHERE session_id = ?').run(sid);
|
||||
db.prepare('DELETE FROM session_questions WHERE session_id = ?').run(sid);
|
||||
db.prepare('DELETE FROM test_sessions WHERE id = ?').run(sid);
|
||||
});
|
||||
|
||||
function deleteSession(req, res, next) {
|
||||
const sid = Number(req.params.id);
|
||||
if (!Number.isInteger(sid) || sid <= 0)
|
||||
return res.status(400).json({ error: 'Invalid session id' });
|
||||
try {
|
||||
const sess = db.prepare('SELECT id, user_id, mode FROM test_sessions WHERE id = ?').get(sid);
|
||||
if (!sess) return res.status(404).json({ error: 'Session not found' });
|
||||
_deleteSessionTx(sid);
|
||||
audit(req, 'session.delete', `session:${sid}`, `user:${sess.user_id} mode:${sess.mode}`);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── DELETE /api/admin/users/:id/sessions ────────────────────────────── */
|
||||
function clearUserSessions(req, res, next) {
|
||||
const uid = Number(req.params.id);
|
||||
@@ -638,7 +665,7 @@ function broadcast(req, res) {
|
||||
module.exports = {
|
||||
getStats, getOverview, globalSearch,
|
||||
getUsers, updateRole, getUserSessions, getAllSessions, getSessionDetail,
|
||||
clearUserSessions, updateUser, banUser, deleteUser,
|
||||
clearUserSessions, deleteSession, updateUser, banUser, deleteUser,
|
||||
getFeatures, updateFeatures, getFreeStudentFeatures, updateFreeStudentFeatures,
|
||||
getAuditLog, clearAuditLog, getErrorLog, clearErrorLog, getHealth,
|
||||
getTopics, createTopic, updateTopic, deleteTopic,
|
||||
|
||||
Reference in New Issue
Block a user