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

O Operator
Diff

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

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

Added
+212
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to b6b3815ce420
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 -- admin cross-entity search.
4#
5# /admin_search.cgi?q=foo
6#
7# Single box. Greps users (email / display name), products (sku / title /
8# asin / walmart_item_id), marketplace_accounts (account_handle),
9# invoices (invoice_number), and subscriptions (id). Returns grouped
10# results with deep links into the existing admin pages.
11#======================================================================
12use strict;
13use warnings;
14
15use lib '/var/www/vhosts/3dshawn.com/repricer.3dshawn.com';
16use CGI;
17use MODS::DBConnect;
18use MODS::Login;
19use MODS::RePricer::Config;
20use MODS::RePricer::Wrapper;
21
22$|=1;
23
24my $q = CGI->new;
25my $form = $q->Vars;
26my $auth = MODS::Login->new;
27my $wrap = MODS::RePricer::Wrapper->new;
28my $db = MODS::DBConnect->new;
29my $cfg = MODS::RePricer::Config->new;
30my $DB = $cfg->settings('database_name');
31
32my $userinfo = $auth->login_verify();
33unless ($userinfo) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; }
34unless ($userinfo->{is_admin}) {
35 print "Status: 302 Found\nLocation: /dashboard.cgi?denied=admin\n\n"; exit;
36}
37
38sub _h { my $s = shift // ''; $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g; $s =~ s/"/&quot;/g; $s }
39sub _q { my $s = shift // ''; $s =~ s/'/''/g; $s }
40
41my $qstr = $form->{q} // '';
42$qstr =~ s/[^a-zA-Z0-9 _\-\.\@:]//g;
43$qstr = substr($qstr, 0, 100);
44
45my $body = '<div style="max-width:980px;margin:0 auto;padding:24px 28px">';
46$body .= ' <div style="font-size:12px;color:#7e92b6;margin-bottom:8px"><a href="/admin.cgi" style="color:#7e92b6">&larr; Admin</a></div>';
47$body .= ' <h1 style="font-size:24px;color:#fff;margin:0 0 12px;font-weight:700">Search</h1>';
48
49# Search box (always visible)
50my $qh = _h($qstr);
51$body .= qq~<form method="GET" style="display:flex;gap:8px;margin-bottom:18px">
52 <input type="text" name="q" value="$qh" placeholder="email, SKU, ASIN, invoice number, account handle..." autofocus
53 style="flex:1;padding:10px 14px;background:#0a0f1f;border:1px solid #1f2a4a;color:#e6ecf6;border-radius:8px;font-size:14px">
54 <button type="submit" style="background:#064e3b;color:#a7f3d0;border:0;padding:10px 22px;border-radius:8px;font-weight:600;cursor:pointer">Search</button>
55 </form>~;
56
57unless (length $qstr >= 2) {
58 $body .= '<div style="color:#7e92b6;font-size:13px;text-align:center;padding:32px">Enter at least 2 characters to search.</div>';
59 $body .= '</div>';
60 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";
61 $wrap->render({ userinfo => $userinfo, page_key => 'admin', title => 'Admin search', body => $body });
62 exit;
63}
64
65my $dbh = $db->db_connect();
66my $like = '%' . _q($qstr) . '%';
67
68# Users
69my $users = $db->db_readwrite_multiple($dbh, qq~
70 SELECT id, email, display_name, is_admin, plan_tier, account_status, created_at,
71 UNIX_TIMESTAMP(created_at) AS created_at_epoch
72 FROM `${DB}`.users
73 WHERE email LIKE '$like' OR display_name LIKE '$like' OR id='$qstr'
74 ORDER BY id DESC LIMIT 25
75~, $ENV{SCRIPT_NAME}, __LINE__);
76
77# Products
78my $products = $db->db_readwrite_multiple($dbh, qq~
79 SELECT p.id, p.sku, p.title, p.marketplace, p.asin, p.walmart_item_id,
80 p.user_id, p.status, p.reprice_enabled,
81 u.email
82 FROM `${DB}`.products p
83 JOIN `${DB}`.users u ON u.id = p.user_id
84 WHERE p.sku LIKE '$like' OR p.title LIKE '$like'
85 OR p.asin LIKE '$like' OR p.walmart_item_id LIKE '$like'
86 ORDER BY p.id DESC LIMIT 25
87~, $ENV{SCRIPT_NAME}, __LINE__);
88
89# Marketplace accounts
90my $accts = $db->db_readwrite_multiple($dbh, qq~
91 SELECT a.id, a.platform, a.account_handle, a.status, a.last_error,
92 u.email, u.id AS user_id
93 FROM `${DB}`.marketplace_accounts a
94 JOIN `${DB}`.users u ON u.id = a.user_id
95 WHERE a.account_handle LIKE '$like'
96 ORDER BY a.id DESC LIMIT 20
97~, $ENV{SCRIPT_NAME}, __LINE__);
98
99# Invoices
100my $invoices = [];
101my $r_inv_check = $db->db_readwrite($dbh, qq~
102 SELECT COUNT(*) AS n FROM information_schema.tables
103 WHERE table_schema=DATABASE() AND table_name='invoices'
104~, $ENV{SCRIPT_NAME}, __LINE__);
105if ($r_inv_check && $r_inv_check->{n}) {
106 $invoices = $db->db_readwrite_multiple($dbh, qq~
107 SELECT i.id, i.invoice_number, i.amount_due_cents, i.status,
108 i.issued_at, UNIX_TIMESTAMP(i.issued_at) AS issued_at_epoch,
109 u.email, u.id AS user_id
110 FROM `${DB}`.invoices i
111 JOIN `${DB}`.users u ON u.id = i.user_id
112 WHERE i.invoice_number LIKE '$like' OR i.id='$qstr'
113 ORDER BY i.id DESC LIMIT 20
114 ~, $ENV{SCRIPT_NAME}, __LINE__);
115}
116
117$db->db_disconnect($dbh);
118
119my $total = scalar(@$users) + scalar(@$products) + scalar(@$accts) + scalar(@$invoices);
120$body .= '<div style="color:#7e92b6;font-size:13px;margin-bottom:14px">'
121 . $total . ' match' . ($total == 1 ? '' : 'es') . " for &quot;$qh&quot;</div>";
122
123# Users section
124if (@$users) {
125 $body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:14px 18px;margin-bottom:12px">';
126 $body .= ' <div style="font-size:11px;letter-spacing:1.5px;color:#14b8a6;text-transform:uppercase;font-weight:600;margin-bottom:8px">Users (' . scalar(@$users) . ')</div>';
127 foreach my $u (@$users) {
128 my $admin_pill = $u->{is_admin} ? '<span style="background:#064e3b22;color:#14b8a6;font-size:10px;padding:2px 6px;border-radius:999px;margin-left:6px">ADMIN</span>' : '';
129 $body .= qq~ <div style="padding:8px 0;border-bottom:1px dashed #1f2a4a;display:flex;align-items:center;gap:10px">
130 <a href="/admin_user.cgi?id=$u->{id}" style="color:#e6ecf6;text-decoration:none;flex:1">
131 <span style="font-weight:600">~ . _h($u->{email}) . qq~</span>$admin_pill
132 <span style="color:#7e92b6;font-size:12px">&middot; ~ . _h($u->{display_name} // '') . qq~ &middot; ~ . _h($u->{plan_tier}) . qq~ &middot; ~ . _h($u->{account_status}) . qq~</span>
133 </a>
134 <span style="color:#7e92b6;font-size:11px">created ~ . do {
135 my $ep = int($u->{created_at_epoch} || 0);
136 my $s = _h($u->{created_at} // '');
137 ($ep > 0 && length $s)
138 ? qq~<span class="ts" data-ts="$ep" data-fmt="datetime">$s</span>~
139 : $s;
140 } . qq~</span>
141 </div>~;
142 }
143 $body .= '</section>';
144}
145
146# Products section
147if (@$products) {
148 $body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:14px 18px;margin-bottom:12px">';
149 $body .= ' <div style="font-size:11px;letter-spacing:1.5px;color:#14b8a6;text-transform:uppercase;font-weight:600;margin-bottom:8px">Products (' . scalar(@$products) . ')</div>';
150 foreach my $p (@$products) {
151 my $reps = $p->{reprice_enabled} ? '<span style="color:#10b981;font-size:11px;margin-left:6px">ON</span>' : '';
152 $body .= qq~ <div style="padding:8px 0;border-bottom:1px dashed #1f2a4a">
153 <a href="/product.cgi?id=$p->{id}" style="color:#e6ecf6;text-decoration:none">
154 <span style="font-family:ui-monospace,Menlo,monospace;color:#14b8a6">~ . _h($p->{sku}) . qq~</span>
155 &middot; <span style="font-weight:600">~ . _h(substr($p->{title} // '', 0, 60)) . qq~</span>
156 &middot; <span style="color:#7e92b6;font-size:12px">~ . _h(ucfirst($p->{marketplace} // '')) . qq~</span>$reps
157 </a>
158 <div style="color:#7e92b6;font-size:11px;margin-top:2px">owner: ~ . _h($p->{email}) . qq~</div>
159 </div>~;
160 }
161 $body .= '</section>';
162}
163
164# Marketplace accounts
165if (@$accts) {
166 $body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:14px 18px;margin-bottom:12px">';
167 $body .= ' <div style="font-size:11px;letter-spacing:1.5px;color:#14b8a6;text-transform:uppercase;font-weight:600;margin-bottom:8px">Marketplace accounts (' . scalar(@$accts) . ')</div>';
168 foreach my $a (@$accts) {
169 my $st_col = $a->{status} eq 'connected' ? '#10b981'
170 : $a->{status} eq 'error' ? '#fca5a5'
171 : '#7e92b6';
172 $body .= qq~ <div style="padding:8px 0;border-bottom:1px dashed #1f2a4a">
173 <span style="color:#14b8a6;font-weight:600">~ . _h(ucfirst($a->{platform})) . qq~</span>
174 &middot; <span style="font-family:ui-monospace,Menlo,monospace;color:#e6ecf6">~ . _h($a->{account_handle}) . qq~</span>
175 &middot; <span style="color:$st_col;font-size:11px;font-weight:600">~ . _h(uc $a->{status}) . qq~</span>
176 <div style="color:#7e92b6;font-size:11px;margin-top:2px">owner: ~ . _h($a->{email}) . qq~</div>
177 </div>~;
178 }
179 $body .= '</section>';
180}
181
182# Invoices
183if (@$invoices) {
184 $body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:14px 18px;margin-bottom:12px">';
185 $body .= ' <div style="font-size:11px;letter-spacing:1.5px;color:#14b8a6;text-transform:uppercase;font-weight:600;margin-bottom:8px">Invoices (' . scalar(@$invoices) . ')</div>';
186 foreach my $i (@$invoices) {
187 my $amt = sprintf('$%.2f', ($i->{amount_due_cents} || 0) / 100);
188 $body .= qq~ <div style="padding:8px 0;border-bottom:1px dashed #1f2a4a">
189 <span style="color:#e6ecf6;font-family:ui-monospace,Menlo,monospace">~ . _h($i->{invoice_number}) . qq~</span>
190 &middot; <span style="color:#14b8a6;font-weight:600">$amt</span>
191 &middot; <span style="color:#7e92b6;font-size:12px">~ . _h($i->{status}) . qq~</span>
192 <div style="color:#7e92b6;font-size:11px;margin-top:2px">~ . _h($i->{email}) . ' &middot; issued ' . do {
193 my $ep = int($i->{issued_at_epoch} || 0);
194 my $s = _h($i->{issued_at} // '');
195 ($ep > 0 && length $s)
196 ? qq~<span class="ts" data-ts="$ep" data-fmt="datetime">$s</span>~
197 : $s;
198 } . qq~</div>
199 </div>~;
200 }
201 $body .= '</section>';
202}
203
204if ($total == 0) {
205 $body .= '<div style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:24px;text-align:center;color:#7e92b6;font-size:14px">No matches.</div>';
206}
207
208$body .= '</div>';
209
210print "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";
211$wrap->render({ userinfo => $userinfo, page_key => 'admin', title => 'Admin search', body => $body });
212exit;