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

O Operator
Diff

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

added on local at 2026-07-11 18:36:30

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