source: Klonkt/src/routes/admin-users.js@ 7963a6e

main
Last change on this file since 7963a6e was 0c2228a, checked in by roboburr <roboburr@…>, 3 months ago

hub: home button actually visible + delete for every user (cascade)

  • topnav: the hub-home brand used .site-title, which is display:none everywhere (the profile header carries the name). Now its own visible class .nav-hub-home -> the hub name is truly shown top-left on /user/ pages, linking to the hub home.
  • admin/users: delete button for EVERY user (except yourself), not only empty accounts. Deletion cascades (sites + posts + playlists + audio + members + comments) atomically in a transaction; god-only, not the last god, not yourself. Confirmation warns when content will be removed.

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 5.6 KB
Line 
1/**
2 * Admin: User management — Phase E.
3 *
4 * GET /admin/users -> list users
5 * POST /admin/users/:id/role -> change role (member/admin/god)
6 * POST /admin/users/:id/delete -> delete (refused if user has owned content)
7 *
8 * Safety rules:
9 * - The system always keeps at least 1 god (you can't demote/delete the last one).
10 * - You can't change your OWN role to non-god (avoid locking yourself out).
11 * - Delete is refused if the user owns any sites or has any posts.
12 * Leaves it to god to reassign content first.
13 */
14
15import express from 'express';
16import db from '../config/database.js';
17import { renderPage } from '../middleware/render.js';
18import { requireGod } from '../middleware/auth.js';
19
20const router = express.Router();
21
22const VALID_ROLES = new Set(['member', 'admin', 'god']);
23
24function godCount() {
25 return db.prepare("SELECT COUNT(*) AS c FROM users WHERE role = 'god'").get().c;
26}
27
28// ==================== LIST ====================
29router.get('/', requireGod, (req, res) => {
30 const users = db.prepare(`
31 SELECT u.id, u.username, u.email, u.role, u.created_at, u.avatar_url, u.readonly,
32 (SELECT COUNT(*) FROM posts p WHERE p.author_id = u.id) AS post_count,
33 (SELECT COUNT(*) FROM sites s WHERE s.owner_id = u.id) AS site_count
34 FROM users u
35 ORDER BY u.created_at DESC
36 `).all();
37
38 renderPage(req, res, 'pages/admin-users', {
39 pageTitle: 'Users',
40 bodyClass: 'on-admin',
41 users,
42 success: req.query.success || null,
43 error: req.query.error || null,
44 });
45});
46
47// ==================== CHANGE ROLE ====================
48router.post('/:id/role', requireGod, (req, res) => {
49 const userId = req.params.id;
50 const newRole = (req.body.role || '').toString();
51 if (!VALID_ROLES.has(newRole)) {
52 return res.redirect('/admin/users?error=Invalid+role');
53 }
54
55 const target = db.prepare('SELECT id, role FROM users WHERE id = ?').get(userId);
56 if (!target) return res.redirect('/admin/users?error=User+not+found');
57
58 // Prevent self-demotion away from god (lock-out protection)
59 if (target.id === req.session.user.id && newRole !== 'god') {
60 return res.redirect('/admin/users?error=' + encodeURIComponent('Cannot demote yourself'));
61 }
62
63 // Prevent removing the last god
64 if (target.role === 'god' && newRole !== 'god' && godCount() <= 1) {
65 return res.redirect('/admin/users?error=' + encodeURIComponent('Cannot demote the only remaining god'));
66 }
67
68 db.prepare('UPDATE users SET role = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
69 .run(newRole, userId);
70 res.redirect('/admin/users?success=' + encodeURIComponent('Role updated'));
71});
72
73// ==================== READ-ONLY (kijk-account) ====================
74router.post('/:id/readonly', requireGod, (req, res) => {
75 const target = db.prepare('SELECT id FROM users WHERE id = ?').get(req.params.id);
76 if (!target) return res.redirect('/admin/users?error=User+not+found');
77 // Niet op jezelf — voorkomt dat een god zichzelf alleen-lezen maakt en vastloopt.
78 if (target.id === req.session.user.id) {
79 return res.redirect('/admin/users?error=' + encodeURIComponent('Kan jezelf niet op alleen-lezen zetten'));
80 }
81 const ro = req.body.readonly === '1' ? 1 : 0;
82 db.prepare('UPDATE users SET readonly = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
83 .run(ro, req.params.id);
84 res.redirect('/admin/users?success=' + encodeURIComponent(ro ? 'Kijk-modus aan' : 'Kijk-modus uit'));
85});
86
87// ==================== DELETE ====================
88router.post('/:id/delete', requireGod, (req, res) => {
89 const userId = req.params.id;
90 const target = db.prepare('SELECT id, role, username FROM users WHERE id = ?').get(userId);
91 if (!target) return res.redirect('/admin/users?error=User+not+found');
92
93 if (target.id === req.session.user.id) {
94 return res.redirect('/admin/users?error=' + encodeURIComponent('Cannot delete yourself'));
95 }
96 if (target.role === 'god' && godCount() <= 1) {
97 return res.redirect('/admin/users?error=' + encodeURIComponent('Cannot delete the only remaining god'));
98 }
99
100 // Cascade-verwijderen: de sites van deze user (+ posts/playlists/audio/leden/
101 // comments daaronder), z'n eigen content elders, en daarna de user zelf.
102 // Atomisch in een transactie — faalt er een FK, dan rolt alles terug.
103 const del = db.transaction(() => {
104 const sites = db.prepare('SELECT id FROM sites WHERE owner_id = ?').all(userId).map((s) => s.id);
105 for (const sid of sites) {
106 db.prepare('DELETE FROM comments WHERE post_id IN (SELECT id FROM posts WHERE site_id = ?)').run(sid);
107 db.prepare('DELETE FROM posts WHERE site_id = ?').run(sid);
108 db.prepare('DELETE FROM playlists WHERE site_id = ?').run(sid);
109 db.prepare('DELETE FROM audio_tracks WHERE site_id = ?').run(sid);
110 db.prepare('DELETE FROM site_members WHERE site_id = ?').run(sid);
111 db.prepare('DELETE FROM sites WHERE id = ?').run(sid);
112 }
113 // Eigen content op andere sites + losse koppelingen.
114 db.prepare('DELETE FROM comments WHERE post_id IN (SELECT id FROM posts WHERE author_id = ?)').run(userId);
115 db.prepare('DELETE FROM posts WHERE author_id = ?').run(userId);
116 db.prepare('DELETE FROM comments WHERE author_id = ?').run(userId);
117 db.prepare('DELETE FROM site_members WHERE user_id = ?').run(userId);
118 db.prepare('DELETE FROM users WHERE id = ?').run(userId);
119 });
120
121 try {
122 del();
123 } catch (e) {
124 console.error('[admin/users delete]', e.message);
125 return res.redirect('/admin/users?error=' + encodeURIComponent('Verwijderen mislukt (mogelijk gekoppelde data).'));
126 }
127
128 res.redirect('/admin/users?success=' + encodeURIComponent('Gebruiker verwijderd: ' + target.username));
129});
130
131export default router;
Note: See TracBrowser for help on using the repository browser.