Diff -- /var/www/vhosts/3dshawn.com/repricer.3dshawn.com/admin_search.cgi
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
to b6b3815ce420
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 | #====================================================================== | |
| 12 | use strict; | |
| 13 | use warnings; | |
| 14 | ||
| 15 | use lib '/var/www/vhosts/3dshawn.com/repricer.3dshawn.com'; | |
| 16 | use CGI; | |
| 17 | use MODS::DBConnect; | |
| 18 | use MODS::Login; | |
| 19 | use MODS::RePricer::Config; | |
| 20 | use MODS::RePricer::Wrapper; | |
| 21 | ||
| 22 | $|=1; | |
| 23 | ||
| 24 | my $q = CGI->new; | |
| 25 | my $form = $q->Vars; | |
| 26 | my $auth = MODS::Login->new; | |
| 27 | my $wrap = MODS::RePricer::Wrapper->new; | |
| 28 | my $db = MODS::DBConnect->new; | |
| 29 | my $cfg = MODS::RePricer::Config->new; | |
| 30 | my $DB = $cfg->settings('database_name'); | |
| 31 | ||
| 32 | my $userinfo = $auth->login_verify(); | |
| 33 | unless ($userinfo) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; } | |
| 34 | unless ($userinfo->{is_admin}) { | |
| 35 | print "Status: 302 Found\nLocation: /dashboard.cgi?denied=admin\n\n"; exit; | |
| 36 | } | |
| 37 | ||
| 38 | sub _h { my $s = shift // ''; $s =~ s/&/&/g; $s =~ s/</</g; $s =~ s/>/>/g; $s =~ s/"/"/g; $s } | |
| 39 | sub _q { my $s = shift // ''; $s =~ s/'/''/g; $s } | |
| 40 | ||
| 41 | my $qstr = $form->{q} // ''; | |
| 42 | $qstr =~ s/[^a-zA-Z0-9 _\-\.\@:]//g; | |
| 43 | $qstr = substr($qstr, 0, 100); | |
| 44 | ||
| 45 | my $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">← 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) | |
| 50 | my $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 | ||
| 57 | unless (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 | ||
| 65 | my $dbh = $db->db_connect(); | |
| 66 | my $like = '%' . _q($qstr) . '%'; | |
| 67 | ||
| 68 | # Users | |
| 69 | my $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 | |
| 78 | my $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 | |
| 90 | my $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 | |
| 100 | my $invoices = []; | |
| 101 | my $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__); | |
| 105 | if ($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 | ||
| 119 | my $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 "$qh"</div>"; | |
| 122 | ||
| 123 | # Users section | |
| 124 | if (@$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">· ~ . _h($u->{display_name} // '') . qq~ · ~ . _h($u->{plan_tier}) . qq~ · ~ . _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 | |
| 147 | if (@$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 | · <span style="font-weight:600">~ . _h(substr($p->{title} // '', 0, 60)) . qq~</span> | |
| 156 | · <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 | |
| 165 | if (@$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 | · <span style="font-family:ui-monospace,Menlo,monospace;color:#e6ecf6">~ . _h($a->{account_handle}) . qq~</span> | |
| 175 | · <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 | |
| 183 | if (@$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 | · <span style="color:#14b8a6;font-weight:600">$amt</span> | |
| 191 | · <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}) . ' · 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 | ||
| 204 | if ($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 | ||
| 210 | 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"; | |
| 211 | $wrap->render({ userinfo => $userinfo, page_key => 'admin', title => 'Admin search', body => $body }); | |
| 212 | exit; |