feat: enhance user management by adding content scrubbing on hard delete and updating dashboard display
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:
+21
-2
@@ -1,6 +1,10 @@
|
|||||||
const db = require('../config/database');
|
const db = require('../config/database');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const uploadsDir = process.env.UPLOADS_DIR || './data/uploads';
|
||||||
|
|
||||||
// ─── Helpers ──────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────
|
||||||
function generateCode(length = 5) {
|
function generateCode(length = 5) {
|
||||||
@@ -150,6 +154,8 @@ const Users = {
|
|||||||
// get username from userId before scrambling
|
// get username from userId before scrambling
|
||||||
const user = this.findById(userId);
|
const user = this.findById(userId);
|
||||||
const scrambled = `_deleted_${user.username}_${Date.now()}`;
|
const scrambled = `_deleted_${user.username}_${Date.now()}`;
|
||||||
|
// Delete uploaded images from disk and clear from DB
|
||||||
|
this._scrubUserContent(userId);
|
||||||
db.prepare('UPDATE users SET username = ?, display_name = ?, password_hash = ?, is_admin = 0, is_organizer = 0 WHERE id = ?')
|
db.prepare('UPDATE users SET username = ?, display_name = ?, password_hash = ?, is_admin = 0, is_organizer = 0 WHERE id = ?')
|
||||||
.run(scrambled, '[deleted]', '', userId);
|
.run(scrambled, '[deleted]', '', userId);
|
||||||
db.prepare('UPDATE password_reset_tokens SET used = 1 WHERE user_id = ?').run(userId);
|
db.prepare('UPDATE password_reset_tokens SET used = 1 WHERE user_id = ?').run(userId);
|
||||||
@@ -157,12 +163,25 @@ const Users = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
hardDeleteUser(userId) {
|
hardDeleteUser(userId) {
|
||||||
|
this._scrubUserContent(userId);
|
||||||
db.prepare('DELETE FROM scans WHERE user_id = ?').run(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 first_scanned_by = 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('UPDATE packages SET last_scanned_by = NULL WHERE last_scanned_by = ?').run(userId);
|
||||||
db.prepare('DELETE FROM password_reset_tokens WHERE user_id = ?').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 sessions WHERE sess LIKE ?").run('%"userId":' + userId + '%');
|
||||||
db.prepare('DELETE FROM users WHERE id = ?').run(userId);
|
db.prepare('DELETE FROM users WHERE id = ?').run(userId);
|
||||||
|
},
|
||||||
|
|
||||||
|
_scrubUserContent(userId) {
|
||||||
|
// Delete images uploaded by this user
|
||||||
|
const images = db.prepare('SELECT first_scan_image FROM packages WHERE first_scanned_by = ? AND first_scan_image IS NOT NULL').all(userId);
|
||||||
|
for (const row of images) {
|
||||||
|
const filePath = path.resolve(uploadsDir, path.basename(row.first_scan_image));
|
||||||
|
try { fs.unlinkSync(filePath); } catch (_) { /* file may already be gone */ }
|
||||||
|
}
|
||||||
|
db.prepare('UPDATE packages SET first_scan_image = NULL WHERE first_scanned_by = ?').run(userId);
|
||||||
|
// Clear hints left by this user
|
||||||
|
db.prepare('UPDATE packages SET last_scan_hint = NULL WHERE last_scanned_by = ?').run(userId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<% if (typeof users !== 'undefined' && users) { users.filter(u => !u.is_admin).forEach(u => { const deleted = u.password_hash === ''; %>
|
<% if (typeof users !== 'undefined' && users) { users.filter(u => !u.is_admin).forEach(u => { const deleted = u.password_hash === ''; %>
|
||||||
<tr<%= deleted ? ' style="opacity: 0.5;"' : '' %>>
|
<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><% if (!deleted) { %><a href="/player/<%= u.username %>"><%= u.display_name %></a><% } else { %><span style="color: var(--muted);"><%= u.display_name %></span><% } %> <span style="color: var(--muted); font-size: 0.8rem;">(<%= u.username %>)</span></td>
|
||||||
<td><%= deleted ? 'Deleted' : u.is_organizer ? 'Organizer' : 'Player' %></td>
|
<td><%= deleted ? 'Deleted' : u.is_organizer ? 'Organizer' : 'Player' %></td>
|
||||||
<td>
|
<td>
|
||||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||||
|
|||||||
Reference in New Issue
Block a user