Diff -- /var/www/vhosts/3dshawn.com/repricer.3dshawn.com/admin_team.cgi

O Operator
Diff

/var/www/vhosts/3dshawn.com/repricer.3dshawn.com/admin_team.cgi

added on local at 2026-07-11 18:33:34

Added
+443
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 5e3ea48b2bc2
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2#======================================================================
3# RePricer - Admin Team Members
4#
5# Any active admin uses this page to add, list, role-edit, and remove
6# fellow platform admins. All admins share one logical team (no
7# sub-admin hierarchy); the only differentiator is which permission
8# group (Manager / Editor / Viewer) gates their access to admin
9# features at template render time.
10#
11# Visibility: admin-only via MODS::RePricer::Admin->require_admin. Non-
12# admin sellers never reach here -- the sidebar entry is admin-only.
13#
14# Self-protection: an admin cannot demote or remove themselves through
15# this page (avoids the "last manager locked out" footgun on a small
16# admin team). They can still adjust their own role via direct SQL or
17# by having another admin update it.
18#======================================================================
19use strict;
20use warnings;
21
22use lib '/var/www/vhosts/3dshawn.com/repricer.3dshawn.com';
23use CGI;
24use MODS::Template;
25use MODS::DBConnect;
26use MODS::Login;
27use MODS::RePricer::Config;
28use MODS::RePricer::Wrapper;
29use MODS::RePricer::Admin;
30
31my $q = CGI->new;
32my $form = $q->Vars;
33my $auth = MODS::Login->new;
34my $wrap = MODS::RePricer::Wrapper->new;
35my $tfile = MODS::Template->new;
36my $db = MODS::DBConnect->new;
37my $cfg = MODS::RePricer::Config->new;
38my $admin = MODS::RePricer::Admin->new;
39my $DB = $cfg->settings('database_name');
40
41$|=1;
42my $userinfo = $auth->login_verify();
43if (!$userinfo) {
44 print "Status: 302 Found\nLocation: /login.cgi\n\n";
45 exit;
46}
47$admin->require_admin($userinfo);
48
49# Resolve whether the *viewer* is super-admin. login_verify doesn't
50# populate users.is_super_admin into the session, so go through the
51# Permissions helper which probes lazily and caches.
52require MODS::RePricer::Permissions;
53my $perm = MODS::RePricer::Permissions->new;
54my $viewer_is_super = $perm->is_super_admin($userinfo) ? 1 : 0;
55
56my $me_uid = $userinfo->{user_id};
57$me_uid =~ s/[^0-9]//g;
58
59sub _esc {
60 my $s = shift;
61 $s //= '';
62 $s =~ s/\\/\\\\/g;
63 $s =~ s/'/''/g;
64 return $s;
65}
66
67sub _h {
68 my $s = shift;
69 $s //= '';
70 $s =~ s/&/&/g;
71 $s =~ s/</&lt;/g;
72 $s =~ s/>/&gt;/g;
73 $s =~ s/"/&quot;/g;
74 return $s;
75}
76
77my $dbh = $db->db_connect();
78
79# ---- Guard: permission_groups must exist before we query it ---------
80my $have_pg_tbl = $db->db_readwrite($dbh, qq~
81 SELECT COUNT(*) AS n FROM information_schema.tables
82 WHERE table_schema='$DB' AND table_name='permission_groups'
83~, $ENV{SCRIPT_NAME}, __LINE__);
84my $perm_ready = ($have_pg_tbl && $have_pg_tbl->{n}) ? 1 : 0;
85
86my $flash_msg = '';
87my $flash_kind = '';
88
89my $action = $form->{action} || '';
90
91if ($action eq 'add_admin' && $perm_ready) {
92 my $email = $form->{email} // '';
93 my $name = $form->{display_name} // '';
94 my $pw = $form->{password} // '';
95 my $role_id = $form->{permission_groups_id} // '';
96
97 $email =~ s/^\s+|\s+$//g;
98 $name =~ s/^\s+|\s+$//g;
99 $role_id =~ s/[^0-9]//g;
100
101 if ($email !~ /\@/ || $name eq '' || length($pw) < 8 || !$role_id) {
102 $flash_msg = 'Email, display name, role, and a password (8+ chars) are required.';
103 $flash_kind = 'error';
104 } else {
105 my $existing = $db->db_readwrite($dbh, qq~
106 SELECT id FROM `${DB}`.users WHERE email='~ . _esc($email) . qq~' LIMIT 1
107 ~, $ENV{SCRIPT_NAME}, __LINE__);
108
109 if ($existing && $existing->{id}) {
110 $flash_msg = 'An account with that email already exists.';
111 $flash_kind = 'error';
112 } else {
113 my $role_ok = $db->db_readwrite($dbh, qq~
114 SELECT id FROM `${DB}`.permission_groups
115 WHERE id='$role_id' AND scope='admin'
116 AND group_status=1
117 AND owner_user_id IS NULL
118 LIMIT 1
119 ~, $ENV{SCRIPT_NAME}, __LINE__);
120
121 if (!$role_ok || !$role_ok->{id}) {
122 $flash_msg = 'Pick a valid admin role.';
123 $flash_kind = 'error';
124 } else {
125 my $new_id = $auth->signup($email, $pw, $name, 'free');
126 if (!$new_id) {
127 $flash_msg = 'Could not create the user (password may be too weak or email rejected).';
128 $flash_kind = 'error';
129 } else {
130 # Mark them admin and attach role. No owner_user_id
131 # for admins -- they're peers, not sub-accounts.
132 $db->db_readwrite($dbh, qq~
133 UPDATE `${DB}`.users
134 SET is_admin=1,
135 permission_groups_id='$role_id'
136 WHERE id='$new_id'
137 ~, $ENV{SCRIPT_NAME}, __LINE__);
138 $flash_msg = "Added admin $email.";
139 $flash_kind = 'success';
140 }
141 }
142 }
143 }
144}
145elsif ($action eq 'update_role' && $perm_ready) {
146 my $member_id = $form->{member_id} // '';
147 my $role_id = $form->{permission_groups_id} // '';
148 $member_id =~ s/[^0-9]//g;
149 $role_id =~ s/[^0-9]//g;
150
151 if ($member_id eq $me_uid) {
152 $flash_msg = "You can't change your own admin role from here.";
153 $flash_kind = 'error';
154 } elsif ($member_id && $role_id) {
155 my $target_ok = $db->db_readwrite($dbh, qq~
156 SELECT id FROM `${DB}`.users
157 WHERE id='$member_id' AND is_admin=1 LIMIT 1
158 ~, $ENV{SCRIPT_NAME}, __LINE__);
159 my $role_ok = $db->db_readwrite($dbh, qq~
160 SELECT id FROM `${DB}`.permission_groups
161 WHERE id='$role_id' AND scope='admin' AND group_status=1 AND owner_user_id IS NULL LIMIT 1
162 ~, $ENV{SCRIPT_NAME}, __LINE__);
163 if ($target_ok && $target_ok->{id} && $role_ok && $role_ok->{id}) {
164 $db->db_readwrite($dbh, qq~
165 UPDATE `${DB}`.users SET permission_groups_id='$role_id' WHERE id='$member_id'
166 ~, $ENV{SCRIPT_NAME}, __LINE__);
167 $flash_msg = 'Role updated.';
168 $flash_kind = 'success';
169 } else {
170 $flash_msg = 'Could not update that admin.';
171 $flash_kind = 'error';
172 }
173 }
174}
175elsif ($action eq 'toggle_super_admin' && $viewer_is_super) {
176 # Promote or revoke super-admin on another admin. Guards:
177 # - self-toggle blocked (you can't demote yourself; would leave no
178 # super-admin if you're the only one)
179 # - target must already be an admin
180 # - last-super-admin guard: demote refused if it would leave zero
181 my $member_id = $form->{member_id} // '';
182 $member_id =~ s/[^0-9]//g;
183 my $want = ($form->{want} && $form->{want} eq '1') ? 1 : 0;
184
185 if (!$member_id || $member_id eq $me_uid) {
186 $flash_msg = "You can't change your own super-admin status from here.";
187 $flash_kind = 'error';
188 } else {
189 my $target = $db->db_readwrite($dbh, qq~
190 SELECT id, is_super_admin FROM `${DB}`.users
191 WHERE id='$member_id' AND is_admin=1 LIMIT 1
192 ~, $ENV{SCRIPT_NAME}, __LINE__);
193 if (!$target || !$target->{id}) {
194 $flash_msg = 'Target admin not found.';
195 $flash_kind = 'error';
196 } elsif (!$want) {
197 # Refuse the demote if it would leave zero super-admins.
198 my $count = $db->db_readwrite($dbh, qq~
199 SELECT COUNT(*) AS n FROM `${DB}`.users
200 WHERE is_super_admin=1 AND id != '$member_id'
201 ~, $ENV{SCRIPT_NAME}, __LINE__);
202 if (!$count || !$count->{n}) {
203 $flash_msg = "Can't demote the last super-admin.";
204 $flash_kind = 'error';
205 } else {
206 $db->db_readwrite($dbh, qq~
207 UPDATE `${DB}`.users
208 SET is_super_admin=0, updated_at=NOW()
209 WHERE id='$member_id'
210 ~, $ENV{SCRIPT_NAME}, __LINE__);
211 $flash_msg = 'Super-admin removed.';
212 $flash_kind = 'success';
213 }
214 } else {
215 $db->db_readwrite($dbh, qq~
216 UPDATE `${DB}`.users
217 SET is_super_admin=1, is_admin=1, updated_at=NOW()
218 WHERE id='$member_id'
219 ~, $ENV{SCRIPT_NAME}, __LINE__);
220 $flash_msg = 'Promoted to super-admin.';
221 $flash_kind = 'success';
222 }
223 }
224}
225elsif ($action eq 'remove_admin') {
226 my $member_id = $form->{member_id} // '';
227 $member_id =~ s/[^0-9]//g;
228
229 if ($member_id eq $me_uid) {
230 $flash_msg = "You can't remove yourself.";
231 $flash_kind = 'error';
232 } elsif ($member_id) {
233 # Belt-and-suspenders: only demote admins, never sellers.
234 my $target_ok = $db->db_readwrite($dbh, qq~
235 SELECT id FROM `${DB}`.users
236 WHERE id='$member_id' AND is_admin=1 LIMIT 1
237 ~, $ENV{SCRIPT_NAME}, __LINE__);
238 if ($target_ok && $target_ok->{id}) {
239 # Two-step: revoke admin + null permission group, then
240 # close the account. Keeps the audit trail intact (we
241 # don't blow the user row away) but blocks future admin
242 # access.
243 $db->db_readwrite($dbh, qq~
244 UPDATE `${DB}`.users
245 SET is_admin=0,
246 permission_groups_id=NULL,
247 account_status='closed'
248 WHERE id='$member_id'
249 ~, $ENV{SCRIPT_NAME}, __LINE__);
250 # Revoke any live sessions so they're kicked immediately.
251 $db->db_readwrite($dbh, qq~
252 UPDATE `${DB}`.user_sessions SET revoked_at=NOW()
253 WHERE user_id='$member_id' AND revoked_at IS NULL
254 ~, $ENV{SCRIPT_NAME}, __LINE__);
255 $flash_msg = 'Admin removed.';
256 $flash_kind = 'success';
257 }
258 }
259}
260
261# ---- Load admin roster ----------------------------------------------
262my @members;
263@members = $db->db_readwrite_multiple($dbh, qq~
264 SELECT u.id, u.email, u.display_name, u.last_login_at,
265 u.permission_groups_id,
266 u.is_super_admin,
267 COALESCE(pg.name, 'Unconstrained') AS role_name,
268 DATE_FORMAT(u.created_at, '%b %e, %Y') AS added_on,
269 DATE_FORMAT(u.last_login_at, '%b %e, %Y') AS last_seen,
270 UNIX_TIMESTAMP(u.created_at) AS added_on_epoch,
271 UNIX_TIMESTAMP(u.last_login_at) AS last_seen_epoch
272 FROM `${DB}`.users u
273 LEFT JOIN `${DB}`.permission_groups pg ON pg.id = u.permission_groups_id
274 WHERE u.is_admin=1 AND u.account_status='active'
275 ORDER BY u.created_at ASC
276~, $ENV{SCRIPT_NAME}, __LINE__) if $perm_ready;
277
278# Fallback when permission_groups doesn't exist yet -- still list the
279# active admins so the page renders something usable on the first deploy.
280if (!$perm_ready) {
281 @members = $db->db_readwrite_multiple($dbh, qq~
282 SELECT id, email, display_name, last_login_at,
283 is_super_admin,
284 DATE_FORMAT(created_at, '%b %e, %Y') AS added_on,
285 DATE_FORMAT(last_login_at, '%b %e, %Y') AS last_seen,
286 UNIX_TIMESTAMP(created_at) AS added_on_epoch,
287 UNIX_TIMESTAMP(last_login_at) AS last_seen_epoch
288 FROM `${DB}`.users
289 WHERE is_admin=1 AND account_status='active'
290 ORDER BY created_at ASC
291 ~, $ENV{SCRIPT_NAME}, __LINE__);
292}
293
294# ---- Load admin roles -----------------------------------------------
295my @roles;
296if ($perm_ready) {
297 @roles = $db->db_readwrite_multiple($dbh, qq~
298 SELECT id, name, system_slug
299 FROM `${DB}`.permission_groups
300 WHERE scope='admin' AND group_status=1 AND owner_user_id IS NULL
301 ORDER BY name ASC
302 ~, $ENV{SCRIPT_NAME}, __LINE__);
303}
304
305# ---- Role -> feature labels for the "What each role can do" panel
306my @role_features;
307if ($perm_ready) {
308 @role_features = $db->db_readwrite_multiple($dbh, qq~
309 SELECT pg.id AS role_id, pg.name AS role_name,
310 pf.label AS feature_label, pf.sort_order
311 FROM `${DB}`.permission_groups pg
312 JOIN `${DB}`.permission_features pf
313 ON FIND_IN_SET(pf.id, pg.permission_features_ids) > 0
314 WHERE pg.scope='admin'
315 AND pg.group_status=1
316 AND pg.owner_user_id IS NULL
317 ORDER BY pg.name ASC, pf.sort_order ASC
318 ~, $ENV{SCRIPT_NAME}, __LINE__);
319}
320
321$db->db_disconnect($dbh);
322
323# ---- Build template vars --------------------------------------------
324my @member_rows;
325foreach my $m (@members) {
326 my @role_opts;
327 foreach my $r (@roles) {
328 push @role_opts, {
329 value => $r->{id},
330 label => _h($r->{name}),
331 selected_attr => ($r->{id} eq ($m->{permission_groups_id} || '') ? ' selected' : ''),
332 };
333 }
334 my $is_self = ($m->{id} eq $me_uid) ? 1 : 0;
335 my $is_super = $m->{is_super_admin} ? 1 : 0;
336 # TF-parity decoration
337 my @_palette_ = ('#5aa9ff','#34d399','#fbbf24','#a78bfa','#f87171','#22c55e','#06b6d4','#ec4899','#f59e0b');
338 my $_av_ = $_palette_[ ($m->{id} || 0) % scalar(@_palette_) ];
339 my $_dn_ = $m->{display_name} || $m->{email} || '?';
340 my @_p_ = split /\s+/, $_dn_, 2;
341 my $_ini_ = uc(substr($_p_[0]||'',0,1)) . uc(substr($_p_[1]||'',0,1));
342 $_ini_ = uc(substr($_dn_,0,2)) unless length $_ini_;
343 push @member_rows, {
344 avatar_color => $_av_,
345 initials => $_ini_,
346 id => $m->{id},
347 email => _h($m->{email}),
348 display_name => _h($m->{display_name}),
349 role_name => _h($m->{role_name} || 'Unconstrained'),
350 added_on => do {
351 my $ep = int($m->{added_on_epoch} || 0);
352 my $s = _h($m->{added_on} || '');
353 ($ep > 0 && length $s)
354 ? qq~<span class="ts" data-ts="$ep" data-fmt="date">$s</span>~
355 : ($s || '-');
356 },
357 last_seen => do {
358 my $ep = int($m->{last_seen_epoch} || 0);
359 my $s = _h($m->{last_seen} || '');
360 ($ep > 0 && length $s)
361 ? qq~<span class="ts" data-ts="$ep" data-fmt="date">$s</span>~
362 : ($s || 'Never');
363 },
364 is_self => $is_self,
365 is_super_admin => $is_super,
366 not_super_admin => $is_super ? 0 : 1,
367 role_options => \@role_opts,
368 # Only render the super-admin toggle on rows other than the
369 # viewer's own (and only if the viewer can flip the bit at all).
370 # The action handler enforces this too.
371 show_super_toggle => ($viewer_is_super && !$is_self) ? 1 : 0,
372 };
373}
374
375my @add_role_opts;
376foreach my $r (@roles) {
377 push @add_role_opts, {
378 value => $r->{id},
379 label => _h($r->{name}),
380 };
381}
382
383# Collapse the flat role-feature join into role-grouped cards.
384my @role_cards;
385my %role_idx;
386foreach my $rf (@role_features) {
387 my $rid = $rf->{role_id};
388 if (!exists $role_idx{$rid}) {
389 push @role_cards, {
390 name => _h($rf->{role_name}),
391 features => [],
392 is_super => 0,
393 };
394 $role_idx{$rid} = $#role_cards;
395 }
396 push @{ $role_cards[ $role_idx{$rid} ]{features} }, { label => _h($rf->{feature_label}) };
397}
398
399# Append a synthetic Super Admin card. This role isn't backed by a
400# permission_groups row -- it's the users.is_super_admin flag -- so
401# its capabilities are hardcoded here. Listed last so the implicit
402# ordering reads "weakest to strongest": Viewer -> Editor -> Manager
403# -> Super Admin.
404push @role_cards, {
405 name => 'Super Admin',
406 is_super => 1,
407 features => [
408 { label => 'Everything in Manager' },
409 { label => 'View platform-wide revenue, MRR / ARR, signups' },
410 { label => 'Edit plan pricing &amp; packages' },
411 { label => 'Manage sandbox test credit cards' },
412 { label => 'Edit Software Configuration (Stripe keys, branding)' },
413 { label => 'Promote / demote other super admins' },
414 ],
415};
416
417my $tvars = {
418 perm_ready => $perm_ready,
419 has_perm_ready => $perm_ready ? 1 : 0,
420 flash_msg => _h($flash_msg),
421 has_flash => $flash_msg ? 1 : 0,
422 flash_is_error => ($flash_kind eq 'error') ? 1 : 0,
423 flash_is_success => ($flash_kind eq 'success') ? 1 : 0,
424 members => \@member_rows,
425 has_members => scalar(@member_rows) ? 1 : 0,
426 member_count => scalar(@member_rows),
427 add_role_options => \@add_role_opts,
428 has_roles => scalar(@add_role_opts) ? 1 : 0,
429 role_cards => \@role_cards,
430 has_role_cards => scalar(@role_cards) ? 1 : 0,
431 viewer_is_super_admin => $viewer_is_super,
432};
433
434print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
435
436my $body = join('', $tfile->template('repricer_admin_team.html', $tvars, $userinfo));
437
438$wrap->render({
439 userinfo => $userinfo,
440 page_key => 'admin_team',
441 title => 'Admin Team',
442 body => $body,
443});