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

O Operator
Diff

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

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

Added
+388
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 58c7e9f79c94
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 - SUPER-ADMIN: All Users (list) + per-user detail
4#
5# Dispatches on SCRIPT_NAME:
6# admin_users.cgi -> list view (TF-mirror layout)
7# admin_user.cgi?u=<id> -> detail view (drill into one seller)
8#
9# Consolidated: former /admin_user.cgi is now a wrapper that does this
10# file; SCRIPT_NAME routes execution.
11#======================================================================
12use strict; use warnings;
13use lib '/var/www/vhosts/3dshawn.com/repricer.3dshawn.com';
14use CGI;
15use MODS::Template;
16use MODS::DBConnect;
17use MODS::Login;
18use MODS::RePricer::Config;
19use MODS::RePricer::Wrapper;
20use MODS::RePricer::Admin;
21
22my $q = CGI->new;
23my $form = $q->Vars;
24my $auth = MODS::Login->new;
25my $wrap = MODS::RePricer::Wrapper->new;
26my $tfile = MODS::Template->new;
27my $db = MODS::DBConnect->new;
28my $cfg = MODS::RePricer::Config->new;
29my $admin = MODS::RePricer::Admin->new;
30my $DB = $cfg->settings('database_name');
31
32$| = 1;
33
34my $userinfo = $auth->login_verify();
35if (!$userinfo) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; }
36$admin->require_admin($userinfo);
37require MODS::RePricer::Permissions;
38MODS::RePricer::Permissions->new->require_feature($userinfo, 'admin_view_users');
39
40my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'admin_users';
41
42if ($entry eq 'admin_user') {
43 _usr_detail();
44 exit;
45}
46
47_usr_list();
48exit;
49
50#======================================================================
51# LIST view (admin_users.cgi)
52#======================================================================
53sub _usr_list {
54 my $sq = $form->{q} || '';
55 $sq =~ s/[^A-Za-z0-9_\-\.\s\@]//g;
56
57 my $dbh = $db->db_connect();
58
59 my $where = '1=1';
60 $where .= " AND (u.email LIKE '%$sq%' OR u.display_name LIKE '%$sq%' OR s.name LIKE '%$sq%' OR s.subdomain LIKE '%$sq%')" if $sq;
61
62 my @users = $db->db_readwrite_multiple($dbh, qq~
63 SELECT u.id, u.email, u.display_name, u.plan_tier, u.account_status,
64 u.is_admin, u.last_login_at, u.created_at,
65 UNIX_TIMESTAMP(u.last_login_at) AS last_login_at_epoch,
66 UNIX_TIMESTAMP(u.created_at) AS created_at_epoch,
67 s.id AS storefront_id, s.name AS storefront_name, s.subdomain
68 FROM `${DB}`.users u
69 LEFT JOIN `${DB}`.storefronts s ON s.user_id = u.id
70 WHERE $where
71 GROUP BY u.id
72 ORDER BY u.created_at DESC
73 LIMIT 200
74 ~, $ENV{SCRIPT_NAME}, __LINE__);
75
76 my @palette = ('#5aa9ff','#34d399','#fbbf24','#a78bfa','#f87171','#22c55e','#06b6d4','#ec4899','#f59e0b');
77 foreach my $u (@users) {
78 my $dn = $u->{display_name} || $u->{email} || ('User #' . $u->{id});
79 $u->{display_name} = _usr_h($dn);
80 $u->{email} = _usr_h($u->{email});
81 $u->{plan_tier} = ucfirst($u->{plan_tier} || 'free');
82 $u->{account_status} = _usr_h($u->{account_status} || 'active');
83 $u->{is_admin_flag} = $u->{is_admin} ? 1 : 0;
84 my $_ll_epoch = int($u->{last_login_at_epoch} || 0);
85 my $_ll_str = $u->{last_login_at} || '-';
86 $u->{last_login_at} = ($_ll_epoch > 0)
87 ? qq~<span class="ts" data-ts="$_ll_epoch" data-fmt="datetime">$_ll_str</span>~
88 : $_ll_str;
89 $u->{storefront_name} = _usr_h($u->{storefront_name} || '-');
90 $u->{avatar_color} = $palette[ ($u->{id} || 0) % scalar(@palette) ];
91 my @parts = split /\s+/, $dn, 2;
92 my $ini = '';
93 $ini .= uc(substr($parts[0], 0, 1)) if defined $parts[0];
94 $ini .= uc(substr($parts[1], 0, 1)) if defined $parts[1] && length $parts[1];
95 $ini = uc(substr($dn, 0, 2)) unless length $ini;
96 $u->{initials} = $ini;
97 $u->{is_self} = ($u->{id} == $userinfo->{user_id}) ? 1 : 0;
98 }
99 $db->db_disconnect($dbh);
100
101 print "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";
102 my $tvars = {
103 users => \@users,
104 has_users => scalar(@users) ? 1 : 0,
105 q => _usr_h($sq),
106 };
107 my $body = join('', $tfile->template('repricer_admin_users.html', $tvars, $userinfo));
108 $wrap->render({
109 userinfo => $userinfo,
110 page_key => 'admin_users',
111 title => 'All Users',
112 body => $body,
113 });
114}
115
116#======================================================================
117# DETAIL view (admin_user.cgi?u=<id>) -- ex-/admin_user.cgi
118#======================================================================
119sub _usr_detail {
120 require MODS::RePricer::Marketplaces;
121 require MODS::RePricer::Billing;
122 my $mp = MODS::RePricer::Marketplaces->new;
123 my $bill = MODS::RePricer::Billing->new;
124
125 my $uid = $form->{u} || 0;
126 $uid =~ s/[^0-9]//g;
127 if (!$uid) {
128 print "Status: 302 Found\nLocation: /admin.cgi\n\n";
129 return;
130 }
131
132 my $dbh = $db->db_connect();
133
134 # ---- The target user ---------------------------------------------
135 my $u = $db->db_readwrite($dbh, qq~
136 SELECT id, email, display_name, plan_tier, account_status, is_admin,
137 trust_level, two_factor_enabled, email_verified_at,
138 last_login_at, default_currency, timezone, created_at, updated_at,
139 UNIX_TIMESTAMP(last_login_at) AS last_login_at_epoch,
140 UNIX_TIMESTAMP(created_at) AS created_at_epoch,
141 UNIX_TIMESTAMP(updated_at) AS updated_at_epoch
142 FROM `${DB}`.users
143 WHERE id='$uid'
144 LIMIT 1
145 ~, $ENV{SCRIPT_NAME}, __LINE__);
146
147 if (!$u || !$u->{id}) {
148 $db->db_disconnect($dbh);
149 print "Status: 302 Found\nLocation: /admin.cgi\n\n";
150 return;
151 }
152
153 # ---- Storefront(s) -----------------------------------------------
154 my @storefronts = $db->db_readwrite_multiple($dbh, qq~
155 SELECT id, name, subdomain, created_at,
156 UNIX_TIMESTAMP(created_at) AS created_at_epoch
157 FROM `${DB}`.storefronts
158 WHERE user_id='$uid'
159 ORDER BY id ASC
160 ~, $ENV{SCRIPT_NAME}, __LINE__);
161 foreach my $sf (@storefronts) {
162 my $ep = int($sf->{created_at_epoch} || 0);
163 my $s = $sf->{created_at} // '';
164 $sf->{created_at} = ($ep > 0 && length $s)
165 ? qq~<span class="ts" data-ts="$ep" data-fmt="datetime">$s</span>~
166 : $s;
167 }
168
169 # ---- Model counts ------------------------------------------------
170 my $m_total = _usr_scalar($dbh,
171 "SELECT COUNT(*) AS n FROM `${DB}`.models WHERE user_id='$uid' AND purged_at IS NULL", 'n');
172 my $m_purged = _usr_scalar($dbh,
173 "SELECT COUNT(*) AS n FROM `${DB}`.models WHERE user_id='$uid' AND purged_at IS NOT NULL", 'n');
174 my $m_published = _usr_scalar($dbh,
175 "SELECT COUNT(*) AS n FROM `${DB}`.models WHERE user_id='$uid' AND status='published' AND purged_at IS NULL", 'n');
176
177 my @models = $db->db_readwrite_multiple($dbh, qq~
178 SELECT id, title, status, visibility, currency, base_price_cents,
179 created_at, updated_at,
180 UNIX_TIMESTAMP(created_at) AS created_at_epoch,
181 UNIX_TIMESTAMP(updated_at) AS updated_at_epoch
182 FROM `${DB}`.models
183 WHERE user_id='$uid' AND purged_at IS NULL
184 ORDER BY updated_at DESC
185 LIMIT 10
186 ~, $ENV{SCRIPT_NAME}, __LINE__);
187
188 foreach my $m (@models) {
189 $m->{price} = '$' . sprintf('%.2f', ($m->{base_price_cents} || 0) / 100);
190 $m->{title} = $m->{title} || ('Model #' . $m->{id});
191 my $ep = int($m->{updated_at_epoch} || 0);
192 my $s = $m->{updated_at} // '';
193 $m->{updated_at} = ($ep > 0 && length $s)
194 ? qq~<span class="ts" data-ts="$ep" data-fmt="datetime">$s</span>~
195 : $s;
196 }
197
198 # ---- Orders / revenue -------------------------------------------
199 my $orders_30d_count = 0;
200 my $orders_30d_cents = 0;
201 my $orders_all_cents = 0;
202 my @recent_orders;
203
204 if (@storefronts) {
205 my $sids = join(',', map { "'$_->{id}'" } @storefronts);
206
207 my $rec30 = $db->db_readwrite($dbh, qq~
208 SELECT COUNT(*) AS n, COALESCE(SUM(total_amount_cents),0) AS cents
209 FROM `${DB}`.orders
210 WHERE storefront_id IN ($sids)
211 AND status='paid'
212 AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
213 ~, $ENV{SCRIPT_NAME}, __LINE__);
214 if ($rec30) {
215 $orders_30d_count = $rec30->{n} || 0;
216 $orders_30d_cents = $rec30->{cents} || 0;
217 }
218
219 my $allr = $db->db_readwrite($dbh, qq~
220 SELECT COALESCE(SUM(total_amount_cents),0) AS cents
221 FROM `${DB}`.orders
222 WHERE storefront_id IN ($sids)
223 AND status='paid'
224 ~, $ENV{SCRIPT_NAME}, __LINE__);
225 $orders_all_cents = ($allr && $allr->{cents}) ? $allr->{cents} : 0;
226
227 @recent_orders = $db->db_readwrite_multiple($dbh, qq~
228 SELECT id, storefront_id, buyer_email, total_amount_cents,
229 currency, status, created_at,
230 UNIX_TIMESTAMP(created_at) AS created_at_epoch
231 FROM `${DB}`.orders
232 WHERE storefront_id IN ($sids)
233 ORDER BY created_at DESC
234 LIMIT 10
235 ~, $ENV{SCRIPT_NAME}, __LINE__);
236
237 foreach my $o (@recent_orders) {
238 $o->{total} = '$' . sprintf('%.2f', ($o->{total_amount_cents} || 0) / 100);
239 my $ep = int($o->{created_at_epoch} || 0);
240 my $s = $o->{created_at} // '';
241 $o->{created_at} = ($ep > 0 && length $s)
242 ? qq~<span class="ts" data-ts="$ep" data-fmt="datetime">$s</span>~
243 : $s;
244 }
245 }
246
247 # ---- Marketplace connections ------------------------------------
248 my @mp_rows;
249 my $have_mp = _usr_scalar($dbh, qq~
250 SELECT COUNT(*) AS n FROM information_schema.tables
251 WHERE table_schema='$DB' AND table_name='marketplace_accounts'
252 ~, 'n');
253 if ($have_mp) {
254 @mp_rows = $db->db_readwrite_multiple($dbh, qq~
255 SELECT platform, status, account_handle, connected_at, last_used_at,
256 UNIX_TIMESTAMP(connected_at) AS connected_at_epoch,
257 UNIX_TIMESTAMP(last_used_at) AS last_used_at_epoch
258 FROM `${DB}`.marketplace_accounts
259 WHERE user_id='$uid'
260 ORDER BY platform ASC
261 ~, $ENV{SCRIPT_NAME}, __LINE__);
262 foreach my $r (@mp_rows) {
263 my $p = $mp->by_slug($r->{platform});
264 $r->{name} = $p ? $p->{name} : ucfirst($r->{platform});
265 my $c_ep = int($r->{connected_at_epoch} || 0);
266 my $l_ep = int($r->{last_used_at_epoch} || 0);
267 my $c_s = $r->{connected_at} || '';
268 my $l_s = $r->{last_used_at} || '';
269 $r->{connected_at} = ($c_ep > 0 && length $c_s)
270 ? qq~<span class="ts" data-ts="$c_ep" data-fmt="datetime">$c_s</span>~
271 : ($c_s || '-');
272 $r->{last_used_at} = ($l_ep > 0 && length $l_s)
273 ? qq~<span class="ts" data-ts="$l_ep" data-fmt="datetime">$l_s</span>~
274 : ($l_s || '-');
275 }
276 }
277
278 my $credit_cents = $bill->credit_balance_cents($db, $dbh, $DB, $uid);
279
280 $db->db_disconnect($dbh);
281
282 # ---- Format / tvars ---------------------------------------------
283 my $is_self = ($uid eq $userinfo->{_admin_user_id} || $uid eq $userinfo->{user_id}) ? 1 : 0;
284
285 my $tvars = {
286 target_id => $u->{id},
287 target_email => _usr_esc($u->{email}),
288 target_display_name => _usr_esc($u->{display_name} || $u->{email}),
289 target_plan_tier => ucfirst($u->{plan_tier} || 'pro'),
290 target_plan_raw => $u->{plan_tier} || 'pro',
291 target_account_status => $u->{account_status} || 'active',
292 target_is_admin => $u->{is_admin} ? 1 : 0,
293 target_is_super_admin => $u->{is_super_admin} ? 1 : 0,
294 target_not_super_admin => $u->{is_super_admin} ? 0 : 1,
295 viewer_is_super_admin => do {
296 require MODS::RePricer::Permissions;
297 MODS::RePricer::Permissions->new->is_super_admin($userinfo) ? 1 : 0;
298 },
299 target_2fa => $u->{two_factor_enabled} ? 'On' : 'Off',
300 target_trust_level => $u->{trust_level} || 0,
301 target_currency => $u->{default_currency} || 'USD',
302 target_timezone => $u->{timezone} || 'UTC',
303 target_created_at => do {
304 my $ep = int($u->{created_at_epoch} || 0);
305 my $s = $u->{created_at} || '';
306 ($ep > 0 && length $s)
307 ? qq~<span class="ts" data-ts="$ep" data-fmt="datetime">$s</span>~
308 : ($s || '-');
309 },
310 target_last_login_at => do {
311 my $ep = int($u->{last_login_at_epoch} || 0);
312 my $s = $u->{last_login_at} || '';
313 ($ep > 0 && length $s)
314 ? qq~<span class="ts" data-ts="$ep" data-fmt="datetime">$s</span>~
315 : ($s || '-');
316 },
317 target_email_verified => $u->{email_verified_at} ? 'Verified' : 'Unverified',
318
319 is_self => $is_self,
320 is_active => (($u->{account_status} || '') eq 'active') ? 1 : 0,
321 is_suspended => (($u->{account_status} || '') eq 'suspended') ? 1 : 0,
322 is_closed => (($u->{account_status} || '') eq 'closed') ? 1 : 0,
323
324 storefronts => \@storefronts,
325 storefront_count => scalar(@storefronts),
326 has_storefront => scalar(@storefronts) ? 1 : 0,
327
328 models => \@models,
329 model_count => scalar(@models),
330 m_total => $m_total,
331 m_published => $m_published,
332 m_purged => $m_purged,
333 has_models => scalar(@models) ? 1 : 0,
334
335 recent_orders => \@recent_orders,
336 has_orders => scalar(@recent_orders) ? 1 : 0,
337 orders_30d_count => $orders_30d_count,
338 orders_30d_revenue => '$' . sprintf('%.2f', $orders_30d_cents / 100),
339 orders_all_revenue => '$' . sprintf('%.2f', $orders_all_cents / 100),
340
341 marketplaces => \@mp_rows,
342 has_marketplaces => scalar(@mp_rows) ? 1 : 0,
343
344 target_credit_cents => $credit_cents,
345 target_credit_dollars => ($credit_cents < 0 ? '-' : '') . '$' . sprintf('%.2f', abs($credit_cents) / 100),
346 target_has_credit => $credit_cents > 0 ? 1 : 0,
347 target_has_debit => $credit_cents < 0 ? 1 : 0,
348 target_no_credit => $credit_cents == 0 ? 1 : 0,
349 };
350
351 print "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";
352
353 my $body = join('', $tfile->template('repricer_admin_user.html', $tvars, $userinfo));
354
355 $wrap->render({
356 userinfo => $userinfo,
357 page_key => 'admin',
358 title => 'Admin: ' . ($u->{display_name} || $u->{email}),
359 body => $body,
360 extra_js => ['/assets/javascript/graphs.js'],
361 });
362}
363
364sub _usr_scalar {
365 my ($dbh, $sql, $key) = @_;
366 my $r = $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__);
367 return ($r && defined $r->{$key}) ? $r->{$key} : 0;
368}
369
370sub _usr_esc {
371 my $s = shift;
372 return '' unless defined $s;
373 $s =~ s/&/&amp;/g;
374 $s =~ s/</&lt;/g;
375 $s =~ s/>/&gt;/g;
376 $s =~ s/"/&quot;/g;
377 return $s;
378}
379
380sub _usr_h {
381 my $s = shift;
382 return '' unless defined $s;
383 $s =~ s/&/&amp;/g;
384 $s =~ s/</&lt;/g;
385 $s =~ s/>/&gt;/g;
386 $s =~ s/"/&quot;/g;
387 return $s;
388}