xgit simple git

nexus

nexus

clone git clone https://kb.hax.al/nexus

admin/user.php

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'; ?>