diff --git a/src/models/index.js b/src/models/index.js index a5b19e9..c0236fa 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -84,7 +84,11 @@ const Users = { }, getAllUsers() { - return db.prepare("SELECT id, username, COALESCE(display_name, username) as display_name, is_admin, is_organizer, created_at FROM users WHERE password_hash != '' ORDER BY COALESCE(display_name, username) ASC").all(); + return db.prepare("SELECT id, username, COALESCE(display_name, username) as display_name, password_hash, is_admin, is_organizer, created_at FROM users ORDER BY password_hash = '' ASC, COALESCE(display_name, username) ASC").all(); + }, + + isDeleted(user) { + return user.password_hash === ''; }, getTotalPoints(userId) { @@ -150,6 +154,15 @@ const Users = { .run(scrambled, '[deleted]', '', userId); db.prepare('UPDATE password_reset_tokens SET used = 1 WHERE user_id = ?').run(userId); db.prepare("DELETE FROM sessions WHERE sess LIKE ?").run('%"userId":' + userId + '%'); + }, + + hardDeleteUser(userId) { + db.prepare('DELETE FROM scans WHERE user_id = ?').run(userId); + db.prepare('UPDATE packages SET first_scanned_by = NULL, first_scan_image = NULL WHERE first_scanned_by = ?').run(userId); + db.prepare('UPDATE packages SET last_scanned_by = NULL, last_scan_hint = NULL WHERE last_scanned_by = ?').run(userId); + db.prepare('DELETE FROM password_reset_tokens WHERE user_id = ?').run(userId); + db.prepare("DELETE FROM sessions WHERE sess LIKE ?").run('%"userId":' + userId + '%'); + db.prepare('DELETE FROM users WHERE id = ?').run(userId); } }; diff --git a/src/routes/admin.js b/src/routes/admin.js index dcb7d8d..abfd3ef 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -190,6 +190,24 @@ router.post('/users/:id/delete', requireAdmin, (req, res) => { res.redirect('/admin'); }); +// ─── Hard-delete user account (admin only) ──────────────── +router.post('/users/:id/hard-delete', requireAdmin, (req, res) => { + const userId = parseInt(req.params.id, 10); + const user = Users.findById(userId); + if (!user) { + req.session.flash = { type: 'danger', message: 'User not found.' }; + return res.redirect('/admin'); + } + if (user.is_admin) { + req.session.flash = { type: 'danger', message: 'Cannot hard-delete an admin account.' }; + return res.redirect('/admin'); + } + + Users.hardDeleteUser(userId); + req.session.flash = { type: 'success', message: `Account "${user.display_name || user.username}" has been permanently removed.` }; + res.redirect('/admin'); +}); + // ─── Generate password reset URL (admin only) ──────────── router.post('/reset-password', requireAdmin, (req, res) => { const { username } = req.body; diff --git a/src/views/admin/dashboard.ejs b/src/views/admin/dashboard.ejs index b5277c3..204dfc4 100644 --- a/src/views/admin/dashboard.ejs +++ b/src/views/admin/dashboard.ejs @@ -43,7 +43,7 @@ @@ -72,12 +72,17 @@