Diff -- /var/www/vhosts/3dshawn.com/abforge.3dshawn.com/admin_team.cgi
Diff
/var/www/vhosts/3dshawn.com/abforge.3dshawn.com/admin_team.cgi
added on local at 2026-07-11 18:36:59
Added
+443
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 3548f147d85d
to 3548f147d85d
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 | # ABForge - 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::ABForge::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 | #====================================================================== | |
| 19 | use strict; | |
| 20 | use warnings; | |
| 21 | ||
| 22 | use lib '/var/www/vhosts/abforge.com/httpdocs'; | |
| 23 | use CGI; | |
| 24 | use MODS::Template; | |
| 25 | use MODS::DBConnect; | |
| 26 | use MODS::Login; | |
| 27 | use MODS::ABForge::Config; | |
| 28 | use MODS::ABForge::Wrapper; | |
| 29 | use MODS::ABForge::Admin; | |
| 30 | ||
| 31 | my $q = CGI->new; | |
| 32 | my $form = $q->Vars; | |
| 33 | my $auth = MODS::Login->new; | |
| 34 | my $wrap = MODS::ABForge::Wrapper->new; | |
| 35 | my $tfile = MODS::Template->new; | |
| 36 | my $db = MODS::DBConnect->new; | |
| 37 | my $cfg = MODS::ABForge::Config->new; | |
| 38 | my $admin = MODS::ABForge::Admin->new; | |
| 39 | my $DB = $cfg->settings('database_name'); | |
| 40 | ||
| 41 | $|=1; | |
| 42 | my $userinfo = $auth->login_verify(); | |
| 43 | if (!$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. | |
| 52 | require MODS::ABForge::Permissions; | |
| 53 | my $perm = MODS::ABForge::Permissions->new; | |
| 54 | my $viewer_is_super = $perm->is_super_admin($userinfo) ? 1 : 0; | |
| 55 | ||
| 56 | my $me_uid = $userinfo->{user_id}; | |
| 57 | $me_uid =~ s/[^0-9]//g; | |
| 58 | ||
| 59 | sub _esc { | |
| 60 | my $s = shift; | |
| 61 | $s //= ''; | |
| 62 | $s =~ s/\\/\\\\/g; | |
| 63 | $s =~ s/'/''/g; | |
| 64 | return $s; | |
| 65 | } | |
| 66 | ||
| 67 | sub _h { | |
| 68 | my $s = shift; | |
| 69 | $s //= ''; | |
| 70 | $s =~ s/&/&/g; | |
| 71 | $s =~ s/</</g; | |
| 72 | $s =~ s/>/>/g; | |
| 73 | $s =~ s/"/"/g; | |
| 74 | return $s; | |
| 75 | } | |
| 76 | ||
| 77 | # ---- Viewer-local timestamp wrapper --------------------------------- | |
| 78 | sub _atm_ts_wrap { | |
| 79 | my ($human, $epoch, $fmt) = @_; | |
| 80 | $fmt = 'datetime' unless defined $fmt && length $fmt; | |
| 81 | return '-' unless defined($human) && length($human); | |
| 82 | my $esc = _h($human); | |
| 83 | return $esc unless $epoch && $epoch =~ /^\d+$/; | |
| 84 | return qq~<span class="ts" data-ts="$epoch" data-fmt="$fmt">$esc</span>~; | |
| 85 | } | |
| 86 | ||
| 87 | my $dbh = $db->db_connect(); | |
| 88 | ||
| 89 | # ---- Guard: permission_groups must exist before we query it --------- | |
| 90 | my $have_pg_tbl = $db->db_readwrite($dbh, qq~ | |
| 91 | SELECT COUNT(*) AS n FROM information_schema.tables | |
| 92 | WHERE table_schema='$DB' AND table_name='permission_groups' | |
| 93 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 94 | my $perm_ready = ($have_pg_tbl && $have_pg_tbl->{n}) ? 1 : 0; | |
| 95 | ||
| 96 | my $flash_msg = ''; | |
| 97 | my $flash_kind = ''; | |
| 98 | ||
| 99 | my $action = $form->{action} || ''; | |
| 100 | ||
| 101 | if ($action eq 'add_admin' && $perm_ready) { | |
| 102 | my $email = $form->{email} // ''; | |
| 103 | my $name = $form->{display_name} // ''; | |
| 104 | my $pw = $form->{password} // ''; | |
| 105 | my $role_id = $form->{permission_groups_id} // ''; | |
| 106 | ||
| 107 | $email =~ s/^\s+|\s+$//g; | |
| 108 | $name =~ s/^\s+|\s+$//g; | |
| 109 | $role_id =~ s/[^0-9]//g; | |
| 110 | ||
| 111 | if ($email !~ /\@/ || $name eq '' || length($pw) < 8 || !$role_id) { | |
| 112 | $flash_msg = 'Email, display name, role, and a password (8+ chars) are required.'; | |
| 113 | $flash_kind = 'error'; | |
| 114 | } else { | |
| 115 | my $existing = $db->db_readwrite($dbh, qq~ | |
| 116 | SELECT id FROM `${DB}`.users WHERE email='~ . _esc($email) . qq~' LIMIT 1 | |
| 117 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 118 | ||
| 119 | if ($existing && $existing->{id}) { | |
| 120 | $flash_msg = 'An account with that email already exists.'; | |
| 121 | $flash_kind = 'error'; | |
| 122 | } else { | |
| 123 | my $role_ok = $db->db_readwrite($dbh, qq~ | |
| 124 | SELECT id FROM `${DB}`.permission_groups | |
| 125 | WHERE id='$role_id' AND scope='admin' | |
| 126 | AND group_status=1 | |
| 127 | AND owner_user_id IS NULL | |
| 128 | LIMIT 1 | |
| 129 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 130 | ||
| 131 | if (!$role_ok || !$role_ok->{id}) { | |
| 132 | $flash_msg = 'Pick a valid admin role.'; | |
| 133 | $flash_kind = 'error'; | |
| 134 | } else { | |
| 135 | my $new_id = $auth->signup($email, $pw, $name, 'free'); | |
| 136 | if (!$new_id) { | |
| 137 | $flash_msg = 'Could not create the user (password may be too weak or email rejected).'; | |
| 138 | $flash_kind = 'error'; | |
| 139 | } else { | |
| 140 | # Mark them admin and attach role. No owner_user_id | |
| 141 | # for admins -- they're peers, not sub-accounts. | |
| 142 | $db->db_readwrite($dbh, qq~ | |
| 143 | UPDATE `${DB}`.users | |
| 144 | SET is_admin=1, | |
| 145 | permission_groups_id='$role_id' | |
| 146 | WHERE id='$new_id' | |
| 147 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 148 | $flash_msg = "Added admin $email."; | |
| 149 | $flash_kind = 'success'; | |
| 150 | } | |
| 151 | } | |
| 152 | } | |
| 153 | } | |
| 154 | } | |
| 155 | elsif ($action eq 'update_role' && $perm_ready) { | |
| 156 | my $member_id = $form->{member_id} // ''; | |
| 157 | my $role_id = $form->{permission_groups_id} // ''; | |
| 158 | $member_id =~ s/[^0-9]//g; | |
| 159 | $role_id =~ s/[^0-9]//g; | |
| 160 | ||
| 161 | if ($member_id eq $me_uid) { | |
| 162 | $flash_msg = "You can't change your own admin role from here."; | |
| 163 | $flash_kind = 'error'; | |
| 164 | } elsif ($member_id && $role_id) { | |
| 165 | my $target_ok = $db->db_readwrite($dbh, qq~ | |
| 166 | SELECT id FROM `${DB}`.users | |
| 167 | WHERE id='$member_id' AND is_admin=1 LIMIT 1 | |
| 168 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 169 | my $role_ok = $db->db_readwrite($dbh, qq~ | |
| 170 | SELECT id FROM `${DB}`.permission_groups | |
| 171 | WHERE id='$role_id' AND scope='admin' AND group_status=1 AND owner_user_id IS NULL LIMIT 1 | |
| 172 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 173 | if ($target_ok && $target_ok->{id} && $role_ok && $role_ok->{id}) { | |
| 174 | $db->db_readwrite($dbh, qq~ | |
| 175 | UPDATE `${DB}`.users SET permission_groups_id='$role_id' WHERE id='$member_id' | |
| 176 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 177 | $flash_msg = 'Role updated.'; | |
| 178 | $flash_kind = 'success'; | |
| 179 | } else { | |
| 180 | $flash_msg = 'Could not update that admin.'; | |
| 181 | $flash_kind = 'error'; | |
| 182 | } | |
| 183 | } | |
| 184 | } | |
| 185 | elsif ($action eq 'toggle_super_admin' && $viewer_is_super) { | |
| 186 | # Promote or revoke super-admin on another admin. Guards: | |
| 187 | # - self-toggle blocked (you can't demote yourself; would leave no | |
| 188 | # super-admin if you're the only one) | |
| 189 | # - target must already be an admin | |
| 190 | # - last-super-admin guard: demote refused if it would leave zero | |
| 191 | my $member_id = $form->{member_id} // ''; | |
| 192 | $member_id =~ s/[^0-9]//g; | |
| 193 | my $want = ($form->{want} && $form->{want} eq '1') ? 1 : 0; | |
| 194 | ||
| 195 | if (!$member_id || $member_id eq $me_uid) { | |
| 196 | $flash_msg = "You can't change your own super-admin status from here."; | |
| 197 | $flash_kind = 'error'; | |
| 198 | } else { | |
| 199 | my $target = $db->db_readwrite($dbh, qq~ | |
| 200 | SELECT id, is_super_admin FROM `${DB}`.users | |
| 201 | WHERE id='$member_id' AND is_admin=1 LIMIT 1 | |
| 202 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 203 | if (!$target || !$target->{id}) { | |
| 204 | $flash_msg = 'Target admin not found.'; | |
| 205 | $flash_kind = 'error'; | |
| 206 | } elsif (!$want) { | |
| 207 | # Refuse the demote if it would leave zero super-admins. | |
| 208 | my $count = $db->db_readwrite($dbh, qq~ | |
| 209 | SELECT COUNT(*) AS n FROM `${DB}`.users | |
| 210 | WHERE is_super_admin=1 AND id != '$member_id' | |
| 211 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 212 | if (!$count || !$count->{n}) { | |
| 213 | $flash_msg = "Can't demote the last super-admin."; | |
| 214 | $flash_kind = 'error'; | |
| 215 | } else { | |
| 216 | $db->db_readwrite($dbh, qq~ | |
| 217 | UPDATE `${DB}`.users | |
| 218 | SET is_super_admin=0, updated_at=NOW() | |
| 219 | WHERE id='$member_id' | |
| 220 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 221 | $flash_msg = 'Super-admin removed.'; | |
| 222 | $flash_kind = 'success'; | |
| 223 | } | |
| 224 | } else { | |
| 225 | $db->db_readwrite($dbh, qq~ | |
| 226 | UPDATE `${DB}`.users | |
| 227 | SET is_super_admin=1, is_admin=1, updated_at=NOW() | |
| 228 | WHERE id='$member_id' | |
| 229 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 230 | $flash_msg = 'Promoted to super-admin.'; | |
| 231 | $flash_kind = 'success'; | |
| 232 | } | |
| 233 | } | |
| 234 | } | |
| 235 | elsif ($action eq 'remove_admin') { | |
| 236 | my $member_id = $form->{member_id} // ''; | |
| 237 | $member_id =~ s/[^0-9]//g; | |
| 238 | ||
| 239 | if ($member_id eq $me_uid) { | |
| 240 | $flash_msg = "You can't remove yourself."; | |
| 241 | $flash_kind = 'error'; | |
| 242 | } elsif ($member_id) { | |
| 243 | # Belt-and-suspenders: only demote admins, never sellers. | |
| 244 | my $target_ok = $db->db_readwrite($dbh, qq~ | |
| 245 | SELECT id FROM `${DB}`.users | |
| 246 | WHERE id='$member_id' AND is_admin=1 LIMIT 1 | |
| 247 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 248 | if ($target_ok && $target_ok->{id}) { | |
| 249 | # Two-step: revoke admin + null permission group, then | |
| 250 | # close the account. Keeps the audit trail intact (we | |
| 251 | # don't blow the user row away) but blocks future admin | |
| 252 | # access. | |
| 253 | $db->db_readwrite($dbh, qq~ | |
| 254 | UPDATE `${DB}`.users | |
| 255 | SET is_admin=0, | |
| 256 | permission_groups_id=NULL, | |
| 257 | account_status='closed' | |
| 258 | WHERE id='$member_id' | |
| 259 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 260 | # Revoke any live sessions so they're kicked immediately. | |
| 261 | $db->db_readwrite($dbh, qq~ | |
| 262 | UPDATE `${DB}`.user_sessions SET revoked_at=NOW() | |
| 263 | WHERE user_id='$member_id' AND revoked_at IS NULL | |
| 264 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 265 | $flash_msg = 'Admin removed.'; | |
| 266 | $flash_kind = 'success'; | |
| 267 | } | |
| 268 | } | |
| 269 | } | |
| 270 | ||
| 271 | # ---- Load admin roster ---------------------------------------------- | |
| 272 | my @members; | |
| 273 | @members = $db->db_readwrite_multiple($dbh, qq~ | |
| 274 | SELECT u.id, u.email, u.display_name, u.last_login_at, | |
| 275 | u.permission_groups_id, | |
| 276 | u.is_super_admin, | |
| 277 | COALESCE(pg.name, 'Unconstrained') AS role_name, | |
| 278 | DATE_FORMAT(u.created_at, '%b %e, %Y') AS added_on, | |
| 279 | DATE_FORMAT(u.last_login_at, '%b %e, %Y') AS last_seen, | |
| 280 | UNIX_TIMESTAMP(u.created_at) AS added_on_epoch, | |
| 281 | UNIX_TIMESTAMP(u.last_login_at) AS last_seen_epoch | |
| 282 | FROM `${DB}`.users u | |
| 283 | LEFT JOIN `${DB}`.permission_groups pg ON pg.id = u.permission_groups_id | |
| 284 | WHERE u.is_admin=1 AND u.account_status='active' | |
| 285 | ORDER BY u.created_at ASC | |
| 286 | ~, $ENV{SCRIPT_NAME}, __LINE__) if $perm_ready; | |
| 287 | ||
| 288 | # Fallback when permission_groups doesn't exist yet -- still list the | |
| 289 | # active admins so the page renders something usable on the first deploy. | |
| 290 | if (!$perm_ready) { | |
| 291 | @members = $db->db_readwrite_multiple($dbh, qq~ | |
| 292 | SELECT id, email, display_name, last_login_at, | |
| 293 | is_super_admin, | |
| 294 | DATE_FORMAT(created_at, '%b %e, %Y') AS added_on, | |
| 295 | DATE_FORMAT(last_login_at, '%b %e, %Y') AS last_seen, | |
| 296 | UNIX_TIMESTAMP(created_at) AS added_on_epoch, | |
| 297 | UNIX_TIMESTAMP(last_login_at) AS last_seen_epoch | |
| 298 | FROM `${DB}`.users | |
| 299 | WHERE is_admin=1 AND account_status='active' | |
| 300 | ORDER BY created_at ASC | |
| 301 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 302 | } | |
| 303 | ||
| 304 | # ---- Load admin roles ----------------------------------------------- | |
| 305 | my @roles; | |
| 306 | if ($perm_ready) { | |
| 307 | @roles = $db->db_readwrite_multiple($dbh, qq~ | |
| 308 | SELECT id, name, system_slug | |
| 309 | FROM `${DB}`.permission_groups | |
| 310 | WHERE scope='admin' AND group_status=1 AND owner_user_id IS NULL | |
| 311 | ORDER BY name ASC | |
| 312 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 313 | } | |
| 314 | ||
| 315 | # ---- Role -> feature labels for the "What each role can do" panel | |
| 316 | my @role_features; | |
| 317 | if ($perm_ready) { | |
| 318 | @role_features = $db->db_readwrite_multiple($dbh, qq~ | |
| 319 | SELECT pg.id AS role_id, pg.name AS role_name, | |
| 320 | pf.label AS feature_label, pf.sort_order | |
| 321 | FROM `${DB}`.permission_groups pg | |
| 322 | JOIN `${DB}`.permission_features pf | |
| 323 | ON FIND_IN_SET(pf.id, pg.permission_features_ids) > 0 | |
| 324 | WHERE pg.scope='admin' | |
| 325 | AND pg.group_status=1 | |
| 326 | AND pg.owner_user_id IS NULL | |
| 327 | ORDER BY pg.name ASC, pf.sort_order ASC | |
| 328 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 329 | } | |
| 330 | ||
| 331 | $db->db_disconnect($dbh); | |
| 332 | ||
| 333 | # ---- Build template vars -------------------------------------------- | |
| 334 | my @member_rows; | |
| 335 | foreach my $m (@members) { | |
| 336 | my @role_opts; | |
| 337 | foreach my $r (@roles) { | |
| 338 | push @role_opts, { | |
| 339 | value => $r->{id}, | |
| 340 | label => _h($r->{name}), | |
| 341 | selected_attr => ($r->{id} eq ($m->{permission_groups_id} || '') ? ' selected' : ''), | |
| 342 | }; | |
| 343 | } | |
| 344 | my $is_self = ($m->{id} eq $me_uid) ? 1 : 0; | |
| 345 | my $is_super = $m->{is_super_admin} ? 1 : 0; | |
| 346 | # TF-parity decoration | |
| 347 | my @_palette_ = ('#5aa9ff','#34d399','#fbbf24','#a78bfa','#f87171','#22c55e','#06b6d4','#ec4899','#f59e0b'); | |
| 348 | my $_av_ = $_palette_[ ($m->{id} || 0) % scalar(@_palette_) ]; | |
| 349 | my $_dn_ = $m->{display_name} || $m->{email} || '?'; | |
| 350 | my @_p_ = split /\s+/, $_dn_, 2; | |
| 351 | my $_ini_ = uc(substr($_p_[0]||'',0,1)) . uc(substr($_p_[1]||'',0,1)); | |
| 352 | $_ini_ = uc(substr($_dn_,0,2)) unless length $_ini_; | |
| 353 | push @member_rows, { | |
| 354 | avatar_color => $_av_, | |
| 355 | initials => $_ini_, | |
| 356 | id => $m->{id}, | |
| 357 | email => _h($m->{email}), | |
| 358 | display_name => _h($m->{display_name}), | |
| 359 | role_name => _h($m->{role_name} || 'Unconstrained'), | |
| 360 | added_on => _atm_ts_wrap($m->{added_on}, $m->{added_on_epoch}, 'date'), | |
| 361 | last_seen => ($m->{last_seen} | |
| 362 | ? _atm_ts_wrap($m->{last_seen}, $m->{last_seen_epoch}, 'datetime') | |
| 363 | : 'Never'), | |
| 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 | ||
| 375 | my @add_role_opts; | |
| 376 | foreach 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. | |
| 384 | my @role_cards; | |
| 385 | my %role_idx; | |
| 386 | foreach 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. | |
| 404 | push @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 & 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 | ||
| 417 | my $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 | ||
| 434 | print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n"; | |
| 435 | ||
| 436 | my $body = join('', $tfile->template('abforge_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 | }); |