feat: implement hard-delete functionality for user accounts and update user management UI
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 30s
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 30s
This commit is contained in:
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<label>Username</label>
|
||||
<select name="username" class="form-control" required>
|
||||
<option value="">Select user...</option>
|
||||
<% if (typeof users !== 'undefined' && users) { users.forEach(u => { %>
|
||||
<% if (typeof users !== 'undefined' && users) { users.filter(u => u.password_hash !== '').forEach(u => { %>
|
||||
<option value=\"<%= u.username %>\"><%= u.display_name %><%= u.is_admin ? ' (admin)' : u.is_organizer ? ' (organizer)' : '' %></option>
|
||||
<% }); } %>
|
||||
</select>
|
||||
@@ -72,12 +72,17 @@
|
||||
<tr><th>User</th><th>Role</th><th>Action</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (typeof users !== 'undefined' && users) { users.filter(u => !u.is_admin).forEach(u => { %>
|
||||
<tr>
|
||||
<td><a href="/player/<%= u.username %>"><%= u.display_name %></a></td>
|
||||
<td><%= u.is_organizer ? 'Organizer' : 'Player' %></td>
|
||||
<% if (typeof users !== 'undefined' && users) { users.filter(u => !u.is_admin).forEach(u => { const deleted = u.password_hash === ''; %>
|
||||
<tr<%= deleted ? ' style="opacity: 0.5;"' : '' %>>
|
||||
<td><% if (!deleted) { %><a href="/player/<%= u.username %>"><%= u.display_name %></a><% } else { %><span style="color: var(--muted);"><%= u.display_name %></span><% } %></td>
|
||||
<td><%= deleted ? 'Deleted' : u.is_organizer ? 'Organizer' : 'Player' %></td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<% if (deleted) { %>
|
||||
<form method="POST" action="/admin/users/<%= u.id %>/hard-delete" style="margin:0;">
|
||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('PERMANENTLY remove this user and ALL their scan data? This cannot be undone.')">Hard Delete</button>
|
||||
</form>
|
||||
<% } else { %>
|
||||
<% if (u.is_organizer) { %>
|
||||
<form method="POST" action="/admin/users/<%= u.id %>/role" style="margin:0;">
|
||||
<input type="hidden" name="role" value="player">
|
||||
@@ -90,8 +95,9 @@
|
||||
</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>
|
||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Delete the account "<%= u.display_name %>"? Their data will be anonymized.')">Delete</button>
|
||||
</form>
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user