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

O Operator
Diff

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

added on local at 2026-07-01 13:46:55

Added
+257
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to b4a01173baec
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# WebSTLs - 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::AffSoft::Admin->require_admin;
8# non-admins land on /dashboard.cgi.
9#======================================================================
10use strict;
11use warnings;
12
13use lib '/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com';
14use CGI;
15use MODS::Template;
16use MODS::DBConnect;
17use MODS::Login;
18use MODS::AffSoft::Config;
19use MODS::AffSoft::Wrapper;
20use MODS::AffSoft::Admin;
21
22my $q = CGI->new;
23my $form = $q->Vars;
24my $auth = MODS::Login->new;
25my $wrap = MODS::AffSoft::Wrapper->new;
26my $tfile = MODS::Template->new;
27my $db = MODS::DBConnect->new;
28my $cfg = MODS::AffSoft::Config->new;
29my $admin = MODS::AffSoft::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::AffSoft::Permissions;
41MODS::AffSoft::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# Programs + conversion counts (cheap full-table scans on small tables).
71# AffSoft is an affiliate platform, not a marketplace; "storefronts" /
72# "models" KPIs from the WebSTLs fork are repurposed here as active
73# programs + total conversions.
74my $sf_total = _scalar($db, $dbh,
75 "SELECT COUNT(*) AS n FROM `${DB}`.affiliate_programs WHERE status='active'", 'n');
76my $m_total = _scalar($db, $dbh,
77 "SELECT COUNT(*) AS n FROM `${DB}`.affiliate_conversions", 'n');
78
79# Platform revenue: commissions on conversions paid out in the last 30
80# days. (Order value is the merchant's revenue, not the platform's; we
81# track commissions because that's what flowed through AffSoft.)
82my $rev_30d_cents = _scalar($db, $dbh, qq~
83 SELECT COALESCE(SUM(commission_cents), 0) AS n
84 FROM `${DB}`.affiliate_conversions
85 WHERE status='paid'
86 AND occurred_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
87~, 'n');
88
89# ---- User search results --------------------------------------------
90# Default sort: most recently created. With a query, search across
91# email, display_name, numeric id, and the user's first program name
92# (where they're a merchant).
93my $where = '';
94if ($q_clean ne '') {
95 $where = qq~
96 WHERE u.email LIKE '$like'
97 OR u.display_name LIKE '$like'
98 OR u.id = '$q_safe'
99 OR p.name LIKE '$like'
100 ~;
101}
102
103my @users = $db->db_readwrite_multiple($dbh, qq~
104 SELECT u.id,
105 u.email,
106 u.display_name,
107 u.plan_tier,
108 u.account_status,
109 u.is_admin,
110 u.last_login_at,
111 u.created_at,
112 MIN(p.id) AS storefront_id,
113 MIN(p.name) AS storefront_name,
114 '' AS storefront_subdomain
115 FROM `${DB}`.users u
116 LEFT JOIN `${DB}`.affiliate_programs p ON p.merchant_user_id = u.id
117 $where
118 GROUP BY u.id
119 ORDER BY u.created_at DESC
120 LIMIT 100
121~, $ENV{SCRIPT_NAME}, __LINE__);
122
123# Decorate for template.
124foreach my $u (@users) {
125 $u->{plan_tier} = ucfirst($u->{plan_tier} || 'free');
126 $u->{is_admin_flag} = $u->{is_admin} ? 1 : 0;
127 $u->{is_suspended} = (($u->{account_status} || '') eq 'suspended') ? 1 : 0;
128 $u->{is_closed} = (($u->{account_status} || '') eq 'closed') ? 1 : 0;
129 $u->{display_name} = $u->{display_name} || $u->{email} || ('User #' . $u->{id});
130 $u->{last_login_at} = $u->{last_login_at} || '-';
131 $u->{created_at} = $u->{created_at} || '-';
132 $u->{storefront_name} = $u->{storefront_name} || '-';
133 # TF-parity decoration (single-row layout)
134 my $palette = ['#5aa9ff','#34d399','#fbbf24','#a78bfa','#f87171','#22c55e','#06b6d4','#ec4899','#f59e0b'];
135 $u->{avatar_color} = $palette->[ ($u->{id} || 0) % scalar(@$palette) ];
136 my $dn = $u->{display_name} || $u->{email} || '?';
137 my @parts = split /\s+/, $dn, 2;
138 my $ini = '';
139 $ini .= uc(substr($parts[0], 0, 1)) if defined $parts[0];
140 $ini .= uc(substr($parts[1], 0, 1)) if defined $parts[1] && length $parts[1];
141 $ini = uc(substr($dn, 0, 2)) unless length $ini;
142 $u->{initials} = $ini;
143 $u->{is_self} = ($u->{id} == $userinfo->{user_id}) ? 1 : 0;
144}
145
146$db->db_disconnect($dbh);
147
148my $tvars = {
149 user_id => $userinfo->{user_id},
150
151 # KPIs
152 u_total => $u_total,
153 u_active => $u_active,
154 u_suspended => $u_suspended,
155 u_admins => $u_admins,
156 u_new_30d => $u_new_30d,
157 sf_total => $sf_total,
158 m_total => $m_total,
159 rev_30d => '$' . sprintf('%.2f', ($rev_30d_cents || 0) / 100),
160
161 # Search state
162 q => _esc($q_clean),
163 result_count => scalar(@users),
164 has_results => scalar(@users) ? 1 : 0,
165 has_query => ($q_clean ne '') ? 1 : 0,
166
167 # Rows
168 users => \@users,
169
170 # Tutorial mode flag. When 1, the template renders the example-data
171 # walkthrough; when 0, the KPIs + user table come from the live DB.
172 tutorial_mode => $tutorial_mode,
173 not_tutorial_mode => $tutorial_mode ? 0 : 1,
174};
175
176# ---- Tutorial-mode overlay ------------------------------------------
177# When ?tutorial=1 we paint sample platform-wide KPIs and a sample user
178# list. None of this touches the DB; numbers are illustrative. The
179# audience here is company staff being onboarded into the admin
180# console -- the sample shows what the page reads like on a populated
181# platform with thousands of users.
182if ($tutorial_mode) {
183 $tvars->{u_total} = '1,247';
184 $tvars->{u_active} = '1,089';
185 $tvars->{u_suspended} = 12;
186 $tvars->{u_admins} = 3;
187 $tvars->{u_new_30d} = 86;
188 $tvars->{sf_total} = '1,154';
189 $tvars->{m_total} = '8,372';
190 $tvars->{rev_30d} = '$42,810.00';
191
192 # Sample user list -- a mix of plans, statuses, and join dates so
193 # every pill variant on the page (free / starter / pro / studio,
194 # active / suspended / closed, admin flag) renders for the walker.
195 $tvars->{users} = [
196 { id => 1247, email => 'rune.caster@example.com', display_name => 'Rune Caster',
197 plan_tier => 'Studio', is_admin_flag => 0, is_suspended => 0, is_closed => 0,
198 account_status => 'active', last_login_at => '2026-05-12 09:14',
199 created_at => '2025-08-03', storefront_name => 'rune-demo.studio' },
200 { id => 1192, email => 'maya.r@example.com', display_name => 'Maya R.',
201 plan_tier => 'Pro', is_admin_flag => 0, is_suspended => 0, is_closed => 0,
202 account_status => 'active', last_login_at => '2026-05-12 07:42',
203 created_at => '2025-11-19', storefront_name => 'shardworks.studio' },
204 { id => 1088, email => 'bench.hammer@example.com', display_name => 'Bench Hammer',
205 plan_tier => 'Starter', is_admin_flag => 0, is_suspended => 0, is_closed => 0,
206 account_status => 'active', last_login_at => '2026-05-11 22:08',
207 created_at => '2026-01-14', storefront_name => 'benchhammer.print' },
208 { id => '987', email => 'priya.t@example.com', display_name => 'Priya T.',
209 plan_tier => 'Pro', is_admin_flag => 0, is_suspended => 0, is_closed => 0,
210 account_status => 'active', last_login_at => '2026-05-11 15:33',
211 created_at => '2025-09-02', storefront_name => 'bridgeworks.co' },
212 { id => '641', email => 'support@webstls.com', display_name => 'Support Bot',
213 plan_tier => 'Free', is_admin_flag => 1, is_suspended => 0, is_closed => 0,
214 account_status => 'active', last_login_at => '2026-05-12 11:02',
215 created_at => '2024-03-01', storefront_name => '-' },
216 { id => '312', email => 'kicked@example.com', display_name => 'Suspended Seller',
217 plan_tier => 'Free', is_admin_flag => 0, is_suspended => 1, is_closed => 0,
218 account_status => 'suspended', last_login_at => '2026-04-28 03:11',
219 created_at => '2024-11-22', storefront_name => 'banned-store' },
220 { id => '204', email => 'old.account@example.com', display_name => 'Closed Account',
221 plan_tier => 'Free', is_admin_flag => 0, is_suspended => 0, is_closed => 1,
222 account_status => 'closed', last_login_at => '2025-12-30 12:00',
223 created_at => '2024-06-15', storefront_name => '-' },
224 ];
225 $tvars->{has_results} = 1;
226 $tvars->{result_count} = scalar @{ $tvars->{users} };
227 $tvars->{has_query} = 0;
228 $tvars->{q} = '';
229}
230
231print "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";
232
233my $body = join('', $tfile->template('webstls_admin.html', $tvars, $userinfo));
234
235$wrap->render({
236 userinfo => $userinfo,
237 page_key => 'admin',
238 title => 'Admin Console',
239 body => $body,
240 extra_js => ['/assets/javascript/graphs.js'],
241});
242
243sub _scalar {
244 my ($db, $dbh, $sql, $key) = @_;
245 my $r = $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__);
246 return ($r && defined $r->{$key}) ? $r->{$key} : 0;
247}
248
249sub _esc {
250 my $s = shift;
251 return '' unless defined $s;
252 $s =~ s/&/&/g;
253 $s =~ s/</&lt;/g;
254 $s =~ s/>/&gt;/g;
255 $s =~ s/"/&quot;/g;
256 return $s;
257}