xgit simple git

nexus

nexus

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

admin/user.php


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