diff --git a/src/models/index.js b/src/models/index.js index 15b6226..75a96d2 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -80,7 +80,7 @@ const Users = { }, getAllUsers() { - return db.prepare('SELECT id, username, is_admin, is_organizer, created_at FROM users ORDER BY username ASC').all(); + return db.prepare("SELECT id, username, is_admin, is_organizer, created_at FROM users WHERE username NOT LIKE '[deleted_%]' ORDER BY username ASC").all(); }, getTotalPoints(userId) { @@ -136,6 +136,14 @@ const Users = { getTotalPlayerCount() { return db.prepare('SELECT COUNT(DISTINCT user_id) as count FROM scans WHERE points_awarded > 0').get().count; + }, + + deleteUser(userId) { + const placeholder = '[deleted_' + userId + ']'; + db.prepare('UPDATE users SET username = ?, password_hash = ?, is_admin = 0, is_organizer = 0 WHERE id = ?') + .run(placeholder, '', 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 + '%'); } }; diff --git a/src/routes/admin.js b/src/routes/admin.js index 56be7fc..66de432 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -172,6 +172,24 @@ router.post('/users/:id/role', requireAdmin, (req, res) => { res.redirect('/admin'); }); +// ─── Delete user account (admin only) ───────────────────── +router.post('/users/:id/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 delete an admin account.' }; + return res.redirect('/admin'); + } + + Users.deleteUser(userId); + req.session.flash = { type: 'success', message: `Account "${user.username}" has been deleted.` }; + res.redirect('/admin'); +}); + // ─── Generate password reset URL (admin only) ──────────── router.post('/reset-password', requireAdmin, (req, res) => { const { username } = req.body; diff --git a/src/routes/hunts.js b/src/routes/hunts.js index 7b8d779..48139cc 100644 --- a/src/routes/hunts.js +++ b/src/routes/hunts.js @@ -136,6 +136,22 @@ router.post('/player/:username/password', requireAuth, (req, res) => { res.redirect(`/player/${user.username}`); }); +// ─── Delete own account ─────────────────────────────────── +router.post('/player/:username/delete', requireAuth, (req, res) => { + const user = Users.findByUsername(req.params.username); + if (!user || user.id !== req.session.userId) { + return res.status(403).render('error', { title: 'Forbidden', message: 'You can only delete your own account.' }); + } + if (user.is_admin) { + req.session.flash = { type: 'danger', message: 'Admin accounts cannot be deleted.' }; + return res.redirect(`/player/${user.username}`); + } + + Users.deleteUser(user.id); + req.session.destroy(); + res.redirect('/'); +}); + // ─── Browse all hunts ───────────────────────────────────── router.get('/hunts', (req, res) => { const allHunts = Hunts.getAll(); diff --git a/src/views/admin/dashboard.ejs b/src/views/admin/dashboard.ejs index ad511c0..e9280e3 100644 --- a/src/views/admin/dashboard.ejs +++ b/src/views/admin/dashboard.ejs @@ -77,6 +77,7 @@ <%= u.username %> <%= u.is_organizer ? 'Organizer' : 'Player' %> +
<% if (u.is_organizer) { %>
@@ -88,6 +89,10 @@
<% } %> +
+ +
+
<% }); } %> diff --git a/src/views/player/profile.ejs b/src/views/player/profile.ejs index bd04587..7c39707 100644 --- a/src/views/player/profile.ejs +++ b/src/views/player/profile.ejs @@ -107,6 +107,14 @@ + +
+
Delete Account
+

Permanently delete your account. Your scan history will be anonymized but preserved in leaderboards. This cannot be undone.

+
+ +
+
<% } %>