add some user delete functionalty
Build Images and Deploy / Update-PROD-Stack (push) Successful in 29s
Build Images and Deploy / Update-PROD-Stack (push) Successful in 29s
This commit is contained in:
+9
-1
@@ -80,7 +80,7 @@ const Users = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getAllUsers() {
|
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) {
|
getTotalPoints(userId) {
|
||||||
@@ -136,6 +136,14 @@ const Users = {
|
|||||||
|
|
||||||
getTotalPlayerCount() {
|
getTotalPlayerCount() {
|
||||||
return db.prepare('SELECT COUNT(DISTINCT user_id) as count FROM scans WHERE points_awarded > 0').get().count;
|
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 + '%');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,24 @@ router.post('/users/:id/role', requireAdmin, (req, res) => {
|
|||||||
res.redirect('/admin');
|
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) ────────────
|
// ─── Generate password reset URL (admin only) ────────────
|
||||||
router.post('/reset-password', requireAdmin, (req, res) => {
|
router.post('/reset-password', requireAdmin, (req, res) => {
|
||||||
const { username } = req.body;
|
const { username } = req.body;
|
||||||
|
|||||||
@@ -136,6 +136,22 @@ router.post('/player/:username/password', requireAuth, (req, res) => {
|
|||||||
res.redirect(`/player/${user.username}`);
|
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 ─────────────────────────────────────
|
// ─── Browse all hunts ─────────────────────────────────────
|
||||||
router.get('/hunts', (req, res) => {
|
router.get('/hunts', (req, res) => {
|
||||||
const allHunts = Hunts.getAll();
|
const allHunts = Hunts.getAll();
|
||||||
|
|||||||
@@ -77,6 +77,7 @@
|
|||||||
<td><a href="/player/<%= u.username %>"><%= u.username %></a></td>
|
<td><a href="/player/<%= u.username %>"><%= u.username %></a></td>
|
||||||
<td><%= u.is_organizer ? 'Organizer' : 'Player' %></td>
|
<td><%= u.is_organizer ? 'Organizer' : 'Player' %></td>
|
||||||
<td>
|
<td>
|
||||||
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||||
<% if (u.is_organizer) { %>
|
<% if (u.is_organizer) { %>
|
||||||
<form method="POST" action="/admin/users/<%= u.id %>/role" style="margin:0;">
|
<form method="POST" action="/admin/users/<%= u.id %>/role" style="margin:0;">
|
||||||
<input type="hidden" name="role" value="player">
|
<input type="hidden" name="role" value="player">
|
||||||
@@ -88,6 +89,10 @@
|
|||||||
<button type="submit" class="btn btn-sm btn-success">Make Organizer</button>
|
<button type="submit" class="btn btn-sm btn-success">Make Organizer</button>
|
||||||
</form>
|
</form>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<form method="POST" action="/admin/users/<%= u.id %>/delete" style="margin:0;">
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Permanently delete the account "<%= u.username %>"? Their data will be anonymized.')">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% }); } %>
|
<% }); } %>
|
||||||
|
|||||||
@@ -107,6 +107,14 @@
|
|||||||
<button type="submit" class="btn btn-primary btn-sm">Change Password</button>
|
<button type="submit" class="btn btn-primary btn-sm">Change Password</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="border: 2px solid var(--danger); margin-top: 1.5rem;">
|
||||||
|
<div class="card-header" style="color: var(--danger);">Delete Account</div>
|
||||||
|
<p style="color: var(--muted); font-size: 0.9rem;">Permanently delete your account. Your scan history will be anonymized but preserved in leaderboards. This cannot be undone.</p>
|
||||||
|
<form method="POST" action="/player/<%= profile.username %>/delete" onsubmit="return confirm('Are you sure you want to permanently delete your account? This cannot be undone.')">
|
||||||
|
<button type="submit" class="btn btn-danger">Delete My Account</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user