1 <?php 2 require_once __DIR__ . '/../includes/bootstrap.php'; 3 must_admin(); 4 $id = (int)get('id'); 5 $user = DB::row('SELECT * FROM users WHERE id=?',[$id]); 6 if (!$user) render_404(); 7 $flash = null; $newpw = null; 8 9 // All available permissions 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 20 if ($_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'); // add | subtract | set 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 // Fire addon hook 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]); // refresh 52 } 53 54 if ($action === 'edit_profile') { 55 // Admin editing another user's profile details 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 // Check uniqueness (excluding current user) 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 // Build permissions array for moderators 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 // Handle new password 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'; 117 include __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> 322 function 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'; ?>