Diff -- /var/www/vhosts/3dshawn.com/abforge.3dshawn.com/admin.cgi

O Operator
Diff

/var/www/vhosts/3dshawn.com/abforge.3dshawn.com/admin.cgi

added on local at 2026-07-01 16:00:50

Added
+252
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to adaa79d83a13
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# ABForge - Admin Console (dashboard)
4#
5# Company-staff-only landing page. Lists platform-wide KPIs and offers
6# a user search that drills into /admin_user.cgi for per-account
7# inspection + control. Gated by MODS::ABForge::Admin->require_admin;
8# non-admins land on /dashboard.cgi.
9#======================================================================
10use strict;
11use warnings;
12
13use lib '/var/www/vhosts/abforge.com/httpdocs';
14use CGI;
15use MODS::Template;
16use MODS::DBConnect;
17use MODS::Login;
18use MODS::ABForge::Config;
19use MODS::ABForge::Wrapper;
20use MODS::ABForge::Admin;
21
22my $q = CGI->new;
23my $form = $q->Vars;
24my $auth = MODS::Login->new;
25my $wrap = MODS::ABForge::Wrapper->new;
26my $tfile = MODS::Template->new;
27my $db = MODS::DBConnect->new;
28my $cfg = MODS::ABForge::Config->new;
29my $admin = MODS::ABForge::Admin->new;
30my $DB = $cfg->settings('database_name');
31
32$| = 1;
33
34my $userinfo = $auth->login_verify();
35if (!$userinfo) {
36 print "Status: 302 Found\nLocation: /login.cgi\n\n";
37 exit;
38}
39$admin->require_admin($userinfo);
40require MODS::ABForge::Permissions;
41MODS::ABForge::Permissions->new->require_feature($userinfo, 'admin_view_users');
42
43# Tutorial mode: when ?tutorial=1, render an example-data walkthrough
44# instead of live KPIs / search. Same pattern as the dashboard +
45# analytics tutorials. Admins-only page, so the audience here is
46# company staff onboarding into the operations console -- the sample
47# data shows what the page reads like on a populated platform.
48my $tutorial_mode = ($form->{tutorial} && $form->{tutorial} eq '1') ? 1 : 0;
49
50# ---- Search input (optional). Free-text against email, display_name,
51# id, or storefront name. Sanitized before going into SQL.
52my $q_raw = defined $form->{q} ? $form->{q} : '';
53my $q_clean = $q_raw;
54$q_clean =~ s/[^A-Za-z0-9._@\-\s]//g;
55$q_clean =~ s/^\s+|\s+$//g;
56my $q_safe = $q_clean;
57$q_safe =~ s/'/''/g; # SQL-escape any apostrophes
58my $like = '%' . $q_safe . '%';
59
60my $dbh = $db->db_connect();
61
62# ---- KPI tiles -------------------------------------------------------
63my $u_total = _scalar($db, $dbh, "SELECT COUNT(*) AS n FROM `${DB}`.users", 'n');
64my $u_active = _scalar($db, $dbh, "SELECT COUNT(*) AS n FROM `${DB}`.users WHERE account_status='active'", 'n');
65my $u_suspended = _scalar($db, $dbh, "SELECT COUNT(*) AS n FROM `${DB}`.users WHERE account_status='suspended'", 'n');
66my $u_admins = _scalar($db, $dbh, "SELECT COUNT(*) AS n FROM `${DB}`.users WHERE is_admin=1", 'n');
67my $u_new_30d = _scalar($db, $dbh,
68 "SELECT COUNT(*) AS n FROM `${DB}`.users WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)", 'n');
69
70# Storefront + model counts (cheap, full-table scans on small tables).
71my $sf_total = _scalar($db, $dbh, "SELECT COUNT(*) AS n FROM `${DB}`.storefronts", 'n');
72my $m_total = _scalar($db, $dbh,
73 "SELECT COUNT(*) AS n FROM `${DB}`.models WHERE purged_at IS NULL", 'n');
74
75# Platform revenue across every active storefront (orders.status='paid'
76# in the last 30 days).
77my $rev_30d_cents = _scalar($db, $dbh, qq~
78 SELECT COALESCE(SUM(total_amount_cents), 0) AS n
79 FROM `${DB}`.orders
80 WHERE status='paid'
81 AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
82~, 'n');
83
84# ---- User search results --------------------------------------------
85# Default sort: most recently created. With a query, search across
86# email, display_name, numeric id, and the user's storefront name.
87my $where = '';
88if ($q_clean ne '') {
89 $where = qq~
90 WHERE u.email LIKE '$like'
91 OR u.display_name LIKE '$like'
92 OR u.id = '$q_safe'
93 OR s.name LIKE '$like'
94 OR s.subdomain LIKE '$like'
95 ~;
96}
97
98my @users = $db->db_readwrite_multiple($dbh, qq~
99 SELECT u.id,
100 u.email,
101 u.display_name,
102 u.plan_tier,
103 u.account_status,
104 u.is_admin,
105 u.last_login_at,
106 u.created_at,
107 s.id AS storefront_id,
108 s.name AS storefront_name,
109 s.subdomain AS storefront_subdomain
110 FROM `${DB}`.users u
111 LEFT JOIN `${DB}`.storefronts s ON s.user_id = u.id
112 $where
113 GROUP BY u.id
114 ORDER BY u.created_at DESC
115 LIMIT 100
116~, $ENV{SCRIPT_NAME}, __LINE__);
117
118# Decorate for template.
119foreach my $u (@users) {
120 $u->{plan_tier} = ucfirst($u->{plan_tier} || 'free');
121 $u->{is_admin_flag} = $u->{is_admin} ? 1 : 0;
122 $u->{is_suspended} = (($u->{account_status} || '') eq 'suspended') ? 1 : 0;
123 $u->{is_closed} = (($u->{account_status} || '') eq 'closed') ? 1 : 0;
124 $u->{display_name} = $u->{display_name} || $u->{email} || ('User #' . $u->{id});
125 $u->{last_login_at} = $u->{last_login_at} || '-';
126 $u->{created_at} = $u->{created_at} || '-';
127 $u->{storefront_name} = $u->{storefront_name} || '-';
128 # TF-parity decoration (single-row layout)
129 my $palette = ['#5aa9ff','#34d399','#fbbf24','#a78bfa','#f87171','#22c55e','#06b6d4','#ec4899','#f59e0b'];
130 $u->{avatar_color} = $palette->[ ($u->{id} || 0) % scalar(@$palette) ];
131 my $dn = $u->{display_name} || $u->{email} || '?';
132 my @parts = split /\s+/, $dn, 2;
133 my $ini = '';
134 $ini .= uc(substr($parts[0], 0, 1)) if defined $parts[0];
135 $ini .= uc(substr($parts[1], 0, 1)) if defined $parts[1] && length $parts[1];
136 $ini = uc(substr($dn, 0, 2)) unless length $ini;
137 $u->{initials} = $ini;
138 $u->{is_self} = ($u->{id} == $userinfo->{user_id}) ? 1 : 0;
139}
140
141$db->db_disconnect($dbh);
142
143my $tvars = {
144 user_id => $userinfo->{user_id},
145
146 # KPIs
147 u_total => $u_total,
148 u_active => $u_active,
149 u_suspended => $u_suspended,
150 u_admins => $u_admins,
151 u_new_30d => $u_new_30d,
152 sf_total => $sf_total,
153 m_total => $m_total,
154 rev_30d => '$' . sprintf('%.2f', ($rev_30d_cents || 0) / 100),
155
156 # Search state
157 q => _esc($q_clean),
158 result_count => scalar(@users),
159 has_results => scalar(@users) ? 1 : 0,
160 has_query => ($q_clean ne '') ? 1 : 0,
161
162 # Rows
163 users => \@users,
164
165 # Tutorial mode flag. When 1, the template renders the example-data
166 # walkthrough; when 0, the KPIs + user table come from the live DB.
167 tutorial_mode => $tutorial_mode,
168 not_tutorial_mode => $tutorial_mode ? 0 : 1,
169};
170
171# ---- Tutorial-mode overlay ------------------------------------------
172# When ?tutorial=1 we paint sample platform-wide KPIs and a sample user
173# list. None of this touches the DB; numbers are illustrative. The
174# audience here is company staff being onboarded into the admin
175# console -- the sample shows what the page reads like on a populated
176# platform with thousands of users.
177if ($tutorial_mode) {
178 $tvars->{u_total} = '1,247';
179 $tvars->{u_active} = '1,089';
180 $tvars->{u_suspended} = 12;
181 $tvars->{u_admins} = 3;
182 $tvars->{u_new_30d} = 86;
183 $tvars->{sf_total} = '1,154';
184 $tvars->{m_total} = '8,372';
185 $tvars->{rev_30d} = '$42,810.00';
186
187 # Sample user list -- a mix of plans, statuses, and join dates so
188 # every pill variant on the page (free / starter / pro / studio,
189 # active / suspended / closed, admin flag) renders for the walker.
190 $tvars->{users} = [
191 { id => 1247, email => 'rune.caster@example.com', display_name => 'Rune Caster',
192 plan_tier => 'Studio', is_admin_flag => 0, is_suspended => 0, is_closed => 0,
193 account_status => 'active', last_login_at => '2026-05-12 09:14',
194 created_at => '2025-08-03', storefront_name => 'rune-demo.studio' },
195 { id => 1192, email => 'maya.r@example.com', display_name => 'Maya R.',
196 plan_tier => 'Pro', is_admin_flag => 0, is_suspended => 0, is_closed => 0,
197 account_status => 'active', last_login_at => '2026-05-12 07:42',
198 created_at => '2025-11-19', storefront_name => 'shardworks.studio' },
199 { id => 1088, email => 'bench.hammer@example.com', display_name => 'Bench Hammer',
200 plan_tier => 'Starter', is_admin_flag => 0, is_suspended => 0, is_closed => 0,
201 account_status => 'active', last_login_at => '2026-05-11 22:08',
202 created_at => '2026-01-14', storefront_name => 'benchhammer.print' },
203 { id => '987', email => 'priya.t@example.com', display_name => 'Priya T.',
204 plan_tier => 'Pro', is_admin_flag => 0, is_suspended => 0, is_closed => 0,
205 account_status => 'active', last_login_at => '2026-05-11 15:33',
206 created_at => '2025-09-02', storefront_name => 'bridgeworks.co' },
207 { id => '641', email => 'support@abforge.com', display_name => 'Support Bot',
208 plan_tier => 'Free', is_admin_flag => 1, is_suspended => 0, is_closed => 0,
209 account_status => 'active', last_login_at => '2026-05-12 11:02',
210 created_at => '2024-03-01', storefront_name => '-' },
211 { id => '312', email => 'kicked@example.com', display_name => 'Suspended Seller',
212 plan_tier => 'Free', is_admin_flag => 0, is_suspended => 1, is_closed => 0,
213 account_status => 'suspended', last_login_at => '2026-04-28 03:11',
214 created_at => '2024-11-22', storefront_name => 'banned-store' },
215 { id => '204', email => 'old.account@example.com', display_name => 'Closed Account',
216 plan_tier => 'Free', is_admin_flag => 0, is_suspended => 0, is_closed => 1,
217 account_status => 'closed', last_login_at => '2025-12-30 12:00',
218 created_at => '2024-06-15', storefront_name => '-' },
219 ];
220 $tvars->{has_results} = 1;
221 $tvars->{result_count} = scalar @{ $tvars->{users} };
222 $tvars->{has_query} = 0;
223 $tvars->{q} = '';
224}
225
226print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
227
228my $body = join('', $tfile->template('abforge_admin.html', $tvars, $userinfo));
229
230$wrap->render({
231 userinfo => $userinfo,
232 page_key => 'admin',
233 title => 'Admin Console',
234 body => $body,
235 extra_js => ['/assets/javascript/graphs.js'],
236});
237
238sub _scalar {
239 my ($db, $dbh, $sql, $key) = @_;
240 my $r = $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__);
241 return ($r && defined $r->{$key}) ? $r->{$key} : 0;
242}
243
244sub _esc {
245 my $s = shift;
246 return '' unless defined $s;
247 $s =~ s/&/&/g;
248 $s =~ s/</&lt;/g;
249 $s =~ s/>/&gt;/g;
250 $s =~ s/"/&quot;/g;
251 return $s;
252}