1<?php
2require_once __DIR__ . '/../includes/bootstrap.php';
3must_admin();
4$id = (int)get('id');
5$user = DB::row('SELECT * FROM users WHERE id=?',[$id]);
6if (!$user) render_404();
7$flash = null; $newpw = null;
8
9
10$ALL_PERMS = [
11 'pin_topics' => 'Pin/Unpin Topics',
12 'close_topics' => 'Close/Open Topics',
13 'delete_posts' => 'Delete Any Post',
14 'edit_posts' => 'Edit Any Post',
15 'manage_users' => 'View User List',
16 'upload_images' => 'Upload Images',
17 'bypass_silence' => 'Bypass Silence',
18];
19
20if ($_SERVER['REQUEST_METHOD']==='POST' && csrf_ok()) {
21 $action = post('action');
22
23 if ($action === 'adjust_karma') {
24 $amount = (int)post('karma_amount');
25 $op = post('karma_op');
26 $reason = sanitise(post('karma_reason'));
27 if (!in_array($op, ['add','subtract','set'])) $op = 'add';
28 if ($amount < 0) $amount = 0;
29 $oldKarma = (int)$user['karma'];
30 if ($op === 'set') {
31 $newKarma = $amount;
32 DB::run('UPDATE users SET karma=? WHERE id=?', [$newKarma, $id]);
33 } elseif ($op === 'add') {
34 $newKarma = $oldKarma + $amount;
35 DB::run('UPDATE users SET karma=karma+? WHERE id=?', [$amount, $id]);
36 } else {
37 $newKarma = max(0, $oldKarma - $amount);
38 DB::run('UPDATE users SET karma=? WHERE id=?', [$newKarma, $id]);
39 }
40 if ($reason) {
41 add_notification($id, 'karma_admin', [
42 'from' => $USER['username'],
43 'change' => $newKarma - $oldKarma,
44 'new' => $newKarma,
45 'reason' => $reason,
46 ]);
47 }
48
49 addon_hook('user_karma_changed', ['user_id'=>$id,'old'=>$oldKarma,'new'=>$newKarma,'by'=>$USER['id']]);
50 $flash = ['ok', "Karma updated: {$oldKarma} β {$newKarma}" . ($reason ? " ({$reason})" : '')];
51 $user = DB::row('SELECT * FROM users WHERE id=?', [$id]);
52 }
53
54 if ($action === 'edit_profile') {
55
56 $newUsername = trim(post('username'));
57 $newEmail = trim(post('email'));
58 $newBio = trim(post('bio'));
59 $newRole = post('role');
60
61 $errs = [];
62 if (strlen($newUsername)<3) $errs[]='Username too short.';
63 if (!filter_var($newEmail, FILTER_VALIDATE_EMAIL)) $errs[]='Invalid email.';
64 if (!in_array($newRole,['member','moderator','admin'])) $newRole='member';
65
66
67 if (!$errs) {
68 $dup = DB::row('SELECT id FROM users WHERE (username=? OR email=?) AND id!=?',[$newUsername,$newEmail,$id]);
69 if ($dup) $errs[]='Username or email already taken.';
70 }
71
72 if (!$errs) {
73
74 $permsArr = [];
75 if ($newRole === 'moderator' || $newRole === 'admin') {
76 foreach (array_keys($ALL_PERMS) as $pkey) {
77 if (!empty($_POST['perm_'.$pkey])) $permsArr[$pkey] = true;
78 }
79 }
80 DB::run('UPDATE users SET username=?,email=?,bio=?,role=?,permissions=? WHERE id=?',
81 [$newUsername,$newEmail,$newBio,$newRole,json_encode($permsArr),$id]);
82
83
84 $np = post('new_password');
85 if ($np) {
86 if (strlen($np)<8) { $flash='Password too short (8+ chars).'; }
87 else {
88 DB::run('UPDATE users SET password=? WHERE id=?',[password_hash($np,PASSWORD_BCRYPT,['cost'=>12]),$id]);
89 }
90 }
91 if (!$flash) { $flash='User profile updated!'; $user=DB::row('SELECT * FROM users WHERE id=?',[$id]); }
92 } else {
93 $flash = implode(' ', $errs);
94 }
95 }
96 elseif ($id !== $USER['id']) {
97 switch ($action) {
98 case 'suspend': DB::run('UPDATE users SET suspended=1 WHERE id=?',[$id]); $flash='User suspended.'; break;
99 case 'unsuspend': DB::run('UPDATE users SET suspended=0 WHERE id=?',[$id]); $flash='User unsuspended.'; break;
100 case 'silence': DB::run('UPDATE users SET silenced=1 WHERE id=?',[$id]); $flash='User silenced.'; break;
101 case 'unsilence': DB::run('UPDATE users SET silenced=0 WHERE id=?',[$id]); $flash='User unsilenced.'; break;
102 case 'resetpw':
103 $newpw = bin2hex(random_bytes(5));
104 DB::run('UPDATE users SET password=? WHERE id=?',[password_hash($newpw,PASSWORD_BCRYPT,['cost'=>12]),$id]);
105 break;
106 }
107 $user = DB::row('SELECT * FROM users WHERE id=?',[$id]);
108 }
109}
110
111$userPerms = json_decode($user['permissions'] ?? '{}', true) ?: [];
112$utopics = DB::rows("SELECT t.*,c.name AS cat FROM topics t JOIN categories c ON c.id=t.category_id WHERE t.user_id=? ORDER BY t.created_at DESC LIMIT 10",[$id]);
113$uposts = DB::rows("SELECT p.*,t.title AS tt,t.slug AS ts FROM posts p JOIN topics t ON t.id=p.topic_id WHERE p.user_id=? AND p.deleted=0 ORDER BY p.created_at DESC LIMIT 10",[$id]);
114
115$PAGE_TITLE = 'User: @'.$user['username'];
116$ADMIN_PAGE = 'users';
117include __DIR__ . '/../views/partials/admin_layout.php';
118?>
119<p style="margin-bottom:16px"><a href="<?= u('admin/users.php') ?>">β Back to Users</a></p>
120
121<?php if ($flash): ?>
122 <div class="alert <?= strpos($flash,'!') ? 'ok' : 'err' ?>"><?= e($flash) ?></div>
123<?php endif; ?>
124<?php if ($newpw): ?>
125 <div class="alert warn" style="font-size:14px">π Password reset! New password: <strong style="font-family:monospace"><?= e($newpw) ?></strong> β share this securely with the user.</div>
126<?php endif; ?>
127
128<div class="admin-two-col" style="margin-bottom:0">
129 <!-- Edit Profile Card -->
130 <div class="acard">
131 <div class="acard-head"><h2>βοΈ Edit Profile</h2></div>
132 <div class="acard-body">
133 <form method="POST">
134 <?= csrf_input() ?>
135 <input type="hidden" name="action" value="edit_profile">
136
137 <div class="fg">
138 <label>Username</label>
139 <input type="text" name="username" class="fi" required value="<?= e($user['username']) ?>" minlength="3" maxlength="30">
140 </div>
141 <div class="fg">
142 <label>Email</label>
143 <input type="email" name="email" class="fi" required value="<?= e($user['email']) ?>">
144 </div>
145 <div class="fg">
146 <label>Bio</label>
147 <textarea name="bio" class="fi" rows="3" maxlength="500"><?= e($user['bio']??'') ?></textarea>
148 </div>
149 <div class="fg">
150 <label>Role</label>
151 <select name="role" class="fi" id="roleSelect" onchange="togglePerms(this.value)" <?= $id===$USER['id']?'disabled':'' ?>>
152 <option value="member" <?= $user['role']==='member' ?'selected':'' ?>>Member</option>
153 <option value="moderator" <?= $user['role']==='moderator' ?'selected':'' ?>>Moderator</option>
154 <option value="admin" <?= $user['role']==='admin' ?'selected':'' ?>>Admin</option>
155 </select>
156 </div>
157
158 <!-- Permissions (shown for moderator) -->
159 <div id="permsBox" style="<?= in_array($user['role'],['moderator','admin'])?'':'display:none' ?>">
160 <div class="fg">
161 <label>Moderator Permissions</label>
162 <div class="perms-grid">
163 <?php foreach ($ALL_PERMS as $pk => $plabel): ?>
164 <label class="perm-chk">
165 <input type="checkbox" name="perm_<?= $pk ?>" value="1"
166 <?= !empty($userPerms[$pk]) ? 'checked' : '' ?>>
167 <?= e($plabel) ?>
168 </label>
169 <?php endforeach; ?>
170 </div>
171 </div>
172 </div>
173
174 <div class="fg">
175 <label>Set New Password <small>(leave blank to keep)</small></label>
176 <input type="text" name="new_password" class="fi" placeholder="New password (8+ chars)" autocomplete="new-password">
177 </div>
178
179 <button type="submit" class="btn-primary">Save Changes</button>
180 </form>
181 </div>
182 </div>
183
184 <!-- Quick Actions Card -->
185 <div>
186 <div class="acard" style="margin-bottom:16px">
187 <div class="acard-head"><h2>π€ User Info</h2></div>
188 <div class="acard-body">
189 <div style="display:flex;align-items:center;gap:14px;margin-bottom:16px">
190 <?php if ($user['avatar']): ?>
191 <img src="<?= e($user['avatar']) ?>" class="av-xl" alt="">
192 <?php else: ?>
193 <span class="av-xl av-init"><?= strtoupper($user['username'][0]) ?></span>
194 <?php endif; ?>
195 <div>
196 <h3 style="margin-bottom:4px">@<?= e($user['username']) ?></h3>
197 <p style="font-size:13px;color:#64748b;margin-bottom:6px"><?= e($user['email']) ?></p>
198 <span class="role-tag role-<?= e($user['role']) ?>"><?= e($user['role']) ?></span>
199 <?php if ($user['suspended']): ?> <span class="stag suspended">Suspended</span><?php endif; ?>
200 </div>
201 </div>
202 <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:13px">
203 <div>π Posts: <strong><?= $user['post_count'] ?></strong></div>
204 <div>π¬ Topics: <strong><?= $user['topic_count'] ?></strong></div>
205 <div>β Karma: <strong><?= $user['karma'] ?></strong></div>
206 <div>π
Joined: <span class="ago" data-ts="<?= e($user['joined_at']) ?>"></span></div>
207 </div>
208 <a href="<?= u('users/profile.php?u='.urlencode($user['username'])) ?>" target="_blank" class="btn-ghost btn-sm" style="margin-top:12px">View Profile β</a>
209 </div>
210 </div>
211
212 <?php if ($id !== $USER['id']): ?>
213 <div class="acard" style="margin-bottom:16px">
214 <div class="acard-head"><h2>β Karma Management</h2>
215 <?php $kTier = karma_tier((int)$user['karma']); ?>
216 <span style="font-size:13px;color:<?= e($kTier['color']) ?>"><?= $kTier['icon'] ?> <?= e($kTier['name']) ?> β <?= number_format((int)$user['karma']) ?> pts</span>
217 </div>
218 <div class="acard-body">
219 <!-- Progress bar -->
220 <div style="margin-bottom:14px">
221 <div style="display:flex;justify-content:space-between;font-size:12px;color:var(--muted);margin-bottom:4px">
222 <span><?= e($kTier['name']) ?> (<?= $kTier['min'] ?>)</span>
223 <?php if ($kTier['next']): ?>
224 <span><?= e(karma_tier($kTier['next'])['name']) ?> (<?= $kTier['next'] ?>)</span>
225 <?php else: ?>
226 <span>Max tier π</span>
227 <?php endif; ?>
228 </div>
229 <div style="height:8px;background:var(--border);border-radius:4px;overflow:hidden">
230 <div style="height:100%;width:<?= $kTier['progress'] ?>%;background:<?= e($kTier['color']) ?>;border-radius:4px;transition:width .4s"></div>
231 </div>
232 <div style="font-size:11px;color:var(--faint);margin-top:3px;text-align:right"><?= $kTier['progress'] ?>% to next tier</div>
233 </div>
234
235 <form method="POST">
236 <?= csrf_input() ?>
237 <input type="hidden" name="action" value="adjust_karma">
238 <div style="display:grid;grid-template-columns:120px 1fr;gap:10px;margin-bottom:10px">
239 <div class="fg" style="margin:0">
240 <label style="font-size:12px">Operation</label>
241 <select name="karma_op" class="fi" style="padding:7px 10px">
242 <option value="add">β Add points</option>
243 <option value="subtract">β Subtract points</option>
244 <option value="set">π― Set exact value</option>
245 </select>
246 </div>
247 <div class="fg" style="margin:0">
248 <label style="font-size:12px">Amount</label>
249 <input type="number" name="karma_amount" class="fi" value="10" min="0" max="999999"
250 style="font-family:var(--mono);font-size:16px;text-align:center">
251 </div>
252 </div>
253 <div class="fg" style="margin-bottom:10px">
254 <label style="font-size:12px">Reason <small style="color:var(--faint)">(optional β notifies user)</small></label>
255 <input type="text" name="karma_reason" class="fi" placeholder="e.g. Helpful answer, code of conduct violationβ¦">
256 </div>
257 <button type="submit" class="btn-primary btn-sm">Update Karma</button>
258 </form>
259 </div>
260 </div>
261
262 <div class="acard">
263 <div class="acard-head"><h2>β‘ Quick Actions</h2></div>
264 <div class="acard-body">
265 <div style="display:flex;flex-wrap:wrap;gap:8px">
266 <?php if (!$user['suspended']): ?>
267 <form method="POST" style="display:inline"><?= csrf_input() ?><input type="hidden" name="action" value="suspend">
268 <button class="btn-warn btn-sm" onclick="return confirm('Suspend this user?')">π« Suspend</button></form>
269 <?php else: ?>
270 <form method="POST" style="display:inline"><?= csrf_input() ?><input type="hidden" name="action" value="unsuspend">
271 <button class="btn-ok btn-sm">β
Unsuspend</button></form>
272 <?php endif; ?>
273 <?php if (!$user['silenced']): ?>
274 <form method="POST" style="display:inline"><?= csrf_input() ?><input type="hidden" name="action" value="silence">
275 <button class="btn-warn btn-sm" onclick="return confirm('Silence this user?')">π Silence</button></form>
276 <?php else: ?>
277 <form method="POST" style="display:inline"><?= csrf_input() ?><input type="hidden" name="action" value="unsilence">
278 <button class="btn-ok btn-sm">π Unsilence</button></form>
279 <?php endif; ?>
280 <form method="POST" style="display:inline"><?= csrf_input() ?><input type="hidden" name="action" value="resetpw">
281 <button class="btn-ghost btn-sm" onclick="return confirm('Generate new random password?')">π Reset PW</button></form>
282 </div>
283 </div>
284 </div>
285 <?php else: ?>
286 <div class="acard"><div class="acard-body"><p style="color:#64748b;font-size:13px">You cannot moderate your own account.</p></div></div>
287 <?php endif; ?>
288 </div>
289</div>
290
291<!-- Recent topics & posts -->
292<div class="admin-two-col" style="margin-top:16px">
293 <div class="acard">
294 <div class="acard-head"><h2>Recent Topics</h2></div>
295 <table class="atable">
296 <thead><tr><th>Title</th><th>Category</th><th>Date</th></tr></thead>
297 <tbody>
298 <?php foreach ($utopics as $t): ?>
299 <tr><td><a href="<?= u('forum/topic.php?slug='.urlencode($t['slug'])) ?>" target="_blank"><?= e(mb_substr($t['title'],0,50)) ?></a></td>
300 <td><?= e($t['cat']) ?></td><td><span class="ago" data-ts="<?= e($t['created_at']) ?>"></span></td></tr>
301 <?php endforeach; ?>
302 <?php if (!$utopics): ?><tr><td colspan="3" style="text-align:center;color:#94a3b8;padding:1.5rem">No topics.</td></tr><?php endif; ?>
303 </tbody>
304 </table>
305 </div>
306 <div class="acard">
307 <div class="acard-head"><h2>Recent Posts</h2></div>
308 <table class="atable">
309 <thead><tr><th>In Topic</th><th>Preview</th><th>Date</th></tr></thead>
310 <tbody>
311 <?php foreach ($uposts as $p): ?>
312 <tr><td><a href="<?= u('forum/topic.php?slug='.urlencode($p['ts']).'#post-'.$p['id']) ?>" target="_blank"><?= e(mb_substr($p['tt'],0,30)) ?></a></td>
313 <td><?= e(mb_substr($p['content'],0,60)) ?>β¦</td><td><span class="ago" data-ts="<?= e($p['created_at']) ?>"></span></td></tr>
314 <?php endforeach; ?>
315 <?php if (!$uposts): ?><tr><td colspan="3" style="text-align:center;color:#94a3b8;padding:1.5rem">No posts.</td></tr><?php endif; ?>
316 </tbody>
317 </table>
318 </div>
319</div>
320
321<script>
322function togglePerms(role) {
323 document.getElementById('permsBox').style.display = (role==='moderator'||role==='admin') ? '' : 'none';
324}
325</script>
326
327<?php include __DIR__ . '/../views/partials/admin_layout_end.php'; ?>