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

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

added on local at 2026-07-01 21:47:08

Added
+650
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 4a81119ed660
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 -- Marketplaces page (Connect Amazon / eBay / Walmart).
4#
5# GET /marketplaces.cgi
6# ?flash=...&flash_kind=... (set by oauth_*.cgi on return)
7#
8# POST /marketplaces.cgi
9# action=connect_oauth platform=amazon|ebay region=us|uk|...
10# -> mints a CSRF state, stashes it on users.oauth_state
11# + users.oauth_marketplace_id, then 302s the browser to the
12# platform's authorize URL.
13#
14# action=save_walmart consumer_id=... private_key=... region=us|ca|mx
15# [channel_type=...]
16# -> upserts the Walmart marketplace_accounts row.
17#
18# action=disconnect platform=amazon|ebay|walmart
19# -> sets status=disconnected, blanks tokens, leaves the row so
20# history (linked products / price changes) is preserved.
21#
22# action=test platform=amazon|ebay|walmart
23# -> calls adapter->test_connection and stashes the result in a
24# flash for display.
25#======================================================================
26use strict;
27use warnings;
28
29use lib '/var/www/vhosts/3dshawn.com/repricer.3dshawn.com';
30use CGI;
31use JSON::PP;
32use MODS::Template;
33use MODS::DBConnect;
34use MODS::Login;
35use MODS::RePricer::Config;
36use MODS::RePricer::Wrapper;
37use MODS::RePricer::Marketplaces;
38use MODS::RePricer::Marketplaces::Amazon;
39use MODS::RePricer::Marketplaces::EBay;
40
41$|=1;
42
43my $q = CGI->new;
44my $form = $q->Vars;
45my $auth = MODS::Login->new;
46my $wrap = MODS::RePricer::Wrapper->new;
47my $db = MODS::DBConnect->new;
48my $cfg = MODS::RePricer::Config->new;
49my $DB = $cfg->settings('database_name');
50
51my $userinfo = $auth->login_verify();
52unless ($userinfo) {
53 # OAuth-callback entries want the login redirect to preserve their
54 # return path so the seller ends up back on /marketplaces.cgi.
55 my $entry_probe = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : '';
56 if ($entry_probe =~ /^oauth_(amazon|ebay)$/) {
57 print "Status: 302 Found\nLocation: /login.cgi?next=/marketplaces.cgi\n\n";
58 } else {
59 print "Status: 302 Found\nLocation: /login.cgi\n\n";
60 }
61 exit;
62}
63my $uid = $userinfo->{user_id}; $uid =~ s/[^0-9]//g;
64
65# SCRIPT_NAME dispatch: OAuth callbacks fold in via the same login path
66# so we don't ship separate scripts for the amazon / ebay return URLs.
67my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'marketplaces';
68if ($entry eq 'oauth_amazon') { _oauth_amazon_cb(); exit; }
69if ($entry eq 'oauth_ebay') { _oauth_ebay_cb(); exit; }
70
71sub _h { my $s = shift // ''; $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g; $s =~ s/"/&quot;/g; $s }
72sub _q { my $s = shift // ''; $s =~ s/'/''/g; $s }
73sub _e { my $s = shift // ''; $s =~ s/([^A-Za-z0-9\-._~])/sprintf('%%%02X', ord($1))/ge; $s }
74
75sub _rand_state {
76 my @c = ('A'..'Z','a'..'z','0'..'9');
77 return join '', map { $c[int(rand @c)] } 1..40;
78}
79
80# ----- POST -----------------------------------------------------------
81if (($ENV{REQUEST_METHOD} || '') eq 'POST') {
82 my $dbh = $db->db_connect();
83 my $act = $form->{action} || '';
84 my $flash = '';
85 my $kind = 'info';
86
87 if ($act eq 'connect_oauth') {
88 my $plat = ($form->{platform} || '') =~ /^(amazon|ebay)$/ ? $1 : '';
89 my $region = ($form->{region} || '') =~ /^[a-z_]{2,12}$/ ? $form->{region} : '';
90 if (!$plat) { $flash = 'Unknown platform.'; $kind = 'error'; }
91 else {
92 my $p = MODS::RePricer::Marketplaces::by_slug($plat);
93 if (!$p) { $flash = "No registry entry for $plat"; $kind = 'error'; }
94 else {
95 my $marketplace_id = '';
96 foreach my $r (@{ $p->{regions} || [] }) {
97 if ($r->{code} eq $region) { $marketplace_id = $r->{marketplace_id}; last; }
98 }
99 $marketplace_id ||= ($p->{regions}[0]{marketplace_id} || '');
100 my $state = _rand_state();
101 my $sq = _q($state);
102 my $mq = _q($marketplace_id);
103 $db->db_readwrite($dbh, qq~
104 UPDATE `${DB}`.users SET oauth_state='$sq', oauth_marketplace_id='$mq' WHERE id='$uid'
105 ~, $ENV{SCRIPT_NAME}, __LINE__);
106
107 my $url;
108 if ($plat eq 'amazon') {
109 $url = MODS::RePricer::Marketplaces::Amazon->authorize_url($state);
110 } elsif ($plat eq 'ebay') {
111 $url = MODS::RePricer::Marketplaces::EBay->authorize_url($state);
112 }
113 if (!$url) {
114 $flash = "$plat OAuth is not configured -- super-admin must set client ID / secret in /admin_config.cgi";
115 $kind = 'error';
116 } else {
117 print "Status: 302 Found\nLocation: $url\n\n";
118 exit;
119 }
120 }
121 }
122 }
123 elsif ($act eq 'save_walmart') {
124 my $cid = $form->{consumer_id} || '';
125 my $key = $form->{private_key} || '';
126 my $region = ($form->{region} || '') =~ /^(us|ca|mx)$/ ? $1 : 'us';
127 my $chan = $form->{channel_type} || '';
128 $cid =~ s/^\s+|\s+\z//g;
129 $key =~ s/\r//g;
130 if ($cid !~ /^[A-Fa-f0-9\-]{8,}$/) {
131 $flash = 'Consumer ID looks wrong -- expected a UUID-like value.'; $kind = 'error';
132 }
133 elsif ($key !~ /-----BEGIN [A-Z ]*PRIVATE KEY-----/) {
134 $flash = 'Private key must be a PEM-formatted RSA key (begins with -----BEGIN ...PRIVATE KEY-----)';
135 $kind = 'error';
136 }
137 else {
138 my %region_to_mp = (us => 'WALMART_US', ca => 'WALMART_CA', mx => 'WALMART_MX');
139 my $mp = $region_to_mp{$region};
140 my $meta_json = JSON::PP::encode_json({
141 marketplace_id => $mp,
142 channel_type => $chan,
143 });
144 my $cidq = _q($cid);
145 my $keyq = _q($key);
146 my $metaq = _q($meta_json);
147 my $existing = $db->db_readwrite($dbh, qq~
148 SELECT id FROM `${DB}`.marketplace_accounts
149 WHERE user_id='$uid' AND platform='walmart' LIMIT 1
150 ~, $ENV{SCRIPT_NAME}, __LINE__);
151 if ($existing && $existing->{id}) {
152 my $id = $existing->{id}; $id =~ s/[^0-9]//g;
153 $db->db_readwrite($dbh, qq~
154 UPDATE `${DB}`.marketplace_accounts
155 SET status='connected',
156 access_token='$keyq',
157 account_handle='$cidq',
158 metadata='$metaq',
159 last_error=NULL,
160 connected_at=COALESCE(connected_at, NOW()),
161 last_used_at=NOW(),
162 updated_at=NOW()
163 WHERE id='$id'
164 ~, $ENV{SCRIPT_NAME}, __LINE__);
165 } else {
166 $db->db_readwrite($dbh, qq~
167 INSERT INTO `${DB}`.marketplace_accounts
168 (user_id, platform, status, access_token, account_handle,
169 metadata, connected_at, last_used_at, created_at)
170 VALUES ('$uid', 'walmart', 'connected', '$keyq', '$cidq',
171 '$metaq', NOW(), NOW(), NOW())
172 ~, $ENV{SCRIPT_NAME}, __LINE__);
173 }
174 $flash = 'Walmart credentials saved.'; $kind = 'success';
175 }
176 }
177 elsif ($act eq 'disconnect') {
178 my $plat = ($form->{platform} || '') =~ /^(amazon|ebay|walmart)$/ ? $1 : '';
179 if (!$plat) { $flash = 'Unknown platform.'; $kind = 'error'; }
180 else {
181 $db->db_readwrite($dbh, qq~
182 UPDATE `${DB}`.marketplace_accounts
183 SET status='disconnected',
184 access_token=NULL, refresh_token=NULL,
185 token_expires_at=NULL,
186 updated_at=NOW()
187 WHERE user_id='$uid' AND platform='$plat'
188 ~, $ENV{SCRIPT_NAME}, __LINE__);
189 $flash = "Disconnected $plat."; $kind = 'warn';
190 }
191 }
192 elsif ($act eq 'test') {
193 my $plat = ($form->{platform} || '') =~ /^(amazon|ebay|walmart)$/ ? $1 : '';
194 if (!$plat) { $flash = 'Unknown platform.'; $kind = 'error'; }
195 else {
196 my $row = $db->db_readwrite($dbh, qq~
197 SELECT * FROM `${DB}`.marketplace_accounts
198 WHERE user_id='$uid' AND platform='$plat' AND status='connected' LIMIT 1
199 ~, $ENV{SCRIPT_NAME}, __LINE__);
200 if (!($row && $row->{id})) {
201 $flash = "$plat is not connected."; $kind = 'warn';
202 } else {
203 my $adapter = MODS::RePricer::Marketplaces::adapter_for($row);
204 if (!$adapter) {
205 $flash = "Couldn't load adapter for $plat"; $kind = 'error';
206 } else {
207 my $r = $adapter->test_connection;
208 if ($r->{ok}) {
209 $flash = "$plat test passed: " . ($r->{info} || 'OK'); $kind = 'success';
210 } else {
211 $flash = "$plat test failed: " . ($r->{error} || 'unknown'); $kind = 'error';
212 }
213 }
214 }
215 }
216 }
217 else {
218 $flash = "Unknown action: $act"; $kind = 'error';
219 }
220
221 my $u = '/marketplaces.cgi';
222 if (length $flash) {
223 $u .= '?flash=' . _e($flash) . '&flash_kind=' . $kind;
224 }
225 print "Status: 302 Found\nLocation: $u\n\n";
226 exit;
227}
228
229# ----- GET ------------------------------------------------------------
230my $dbh = $db->db_connect();
231my $accounts_q = $db->db_readwrite_multiple($dbh, qq~
232 SELECT * FROM `${DB}`.marketplace_accounts
233 WHERE user_id='$uid'
234 ORDER BY platform, id
235~, $ENV{SCRIPT_NAME}, __LINE__);
236# Group by platform -- a seller might have multiple Amazon (US + UK + JP)
237# or eBay (US + UK) accounts. Show them all.
238my %by_plat; # platform => arrayref of account rows
239foreach my $r (@$accounts_q) {
240 push @{ $by_plat{$r->{platform}} //= [] }, $r;
241}
242
243my $flash = _h($form->{flash} // '');
244my $flash_kind = ($form->{flash_kind} // 'info');
245$flash_kind = 'info' unless $flash_kind =~ /^(info|success|warn|error)$/;
246
247# Build the per-platform cards HTML inline. This page is bespoke
248# enough that doing it inline beats parameterizing a template.
249my $cards = '';
250foreach my $p (MODS::RePricer::Marketplaces::platforms()) {
251 my $slug = $p->{slug};
252 my $accts = $by_plat{$slug} || []; # all accounts on this platform
253 my $primary = $accts->[0]; # used for the card header summary
254
255 # Card summary state reflects the BEST-status account; if any is
256 # connected we show "Connected (N accounts)".
257 my $status_label = 'Not connected';
258 my $status_class = 'gray';
259 my $any_connected = 0;
260 foreach my $a (@$accts) {
261 my $st = $a->{status} // '';
262 if ($st eq 'connected') { $any_connected = 1; last; }
263 }
264 if ($any_connected) {
265 $status_class = 'green';
266 $status_label = (@$accts > 1) ? 'Connected (' . scalar(@$accts) . ' accounts)' : 'Connected';
267 } elsif (@$accts) {
268 my $st = $primary->{status} // '';
269 if ($st eq 'expired') { $status_label = 'Token expired';$status_class = 'amber'; }
270 elsif ($st eq 'error') { $status_label = 'Error'; $status_class = 'red'; }
271 elsif ($st eq 'disconnected'){ $status_label = 'Disconnected'; $status_class = 'gray'; }
272 }
273
274 # Aggregate display values from the primary account (or empty).
275 my $handle = $primary ? _h($primary->{account_handle} // '') : '';
276 my $last_err = $primary ? _h($primary->{last_error} // '') : '';
277 my $last_used = $primary ? _h($primary->{last_used_at} // '') : '';
278 # Keep an `$acct` alias for the rest of the original code so test +
279 # disconnect actions still fire against the primary connected account.
280 my $acct = $primary;
281
282 # Region select for OAuth platforms.
283 my $region_select = '';
284 foreach my $r (@{ $p->{regions} || [] }) {
285 my $code = _h($r->{code});
286 my $lbl = _h($r->{label});
287 $region_select .= qq~<option value="$code">$lbl</option>~;
288 }
289
290 my $controls = '';
291 if ($p->{auth} eq 'oauth') {
292 my $btn_label = $acct && ($acct->{status} // '') eq 'connected' ? 'Reconnect' : 'Connect';
293 $controls .= <<HTML;
294<form method="POST" action="/marketplaces.cgi" style="display:flex;gap:8px;align-items:center;margin:12px 0 8px">
295 <input type="hidden" name="action" value="connect_oauth">
296 <input type="hidden" name="platform" value="$slug">
297 <select name="region" style="padding:6px 8px;background:#0b1226;border:1px solid #1f2a4a;color:#e6ecf6;border-radius:6px">
298 $region_select
299 </select>
300 <button type="submit" class="btn btn-primary btn-sm">$btn_label $p->{name}</button>
301</form>
302HTML
303 } elsif ($p->{auth} eq 'api_key' && $slug eq 'walmart') {
304 my $cur_handle = $handle ? "value=\"$handle\"" : '';
305 $controls .= <<HTML;
306<form method="POST" action="/marketplaces.cgi" style="margin:12px 0 8px">
307 <input type="hidden" name="action" value="save_walmart">
308 <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:6px">
309 <label style="flex:1;min-width:240px">
310 <span style="display:block;font-size:11px;color:#7e92b6;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px">Consumer ID (UUID)</span>
311 <input type="text" name="consumer_id" $cur_handle required
312 placeholder="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
313 style="width:100%;padding:7px 10px;background:#0b1226;border:1px solid #1f2a4a;color:#e6ecf6;border-radius:6px">
314 </label>
315 <label style="width:120px">
316 <span style="display:block;font-size:11px;color:#7e92b6;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px">Region</span>
317 <select name="region" style="width:100%;padding:7px 8px;background:#0b1226;border:1px solid #1f2a4a;color:#e6ecf6;border-radius:6px">
318 <option value="us">US</option>
319 <option value="ca">Canada</option>
320 <option value="mx">Mexico</option>
321 </select>
322 </label>
323 <label style="width:200px">
324 <span style="display:block;font-size:11px;color:#7e92b6;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px">Channel Type (optional)</span>
325 <input type="text" name="channel_type"
326 placeholder="leave blank if unsure"
327 style="width:100%;padding:7px 10px;background:#0b1226;border:1px solid #1f2a4a;color:#e6ecf6;border-radius:6px">
328 </label>
329 </div>
330 <label style="display:block;margin-bottom:8px">
331 <span style="display:block;font-size:11px;color:#7e92b6;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px">Private Key (PEM)</span>
332 <textarea name="private_key" rows="6" required
333 placeholder="-----BEGIN PRIVATE KEY-----&#10;...&#10;-----END PRIVATE KEY-----"
334 style="width:100%;padding:8px 10px;background:#0b1226;border:1px solid #1f2a4a;color:#e6ecf6;border-radius:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px"></textarea>
335 </label>
336 <button type="submit" class="btn btn-primary btn-sm">Save Walmart credentials</button>
337</form>
338HTML
339 }
340
341 # Test + Disconnect row for an existing connected account.
342 my $actions_row = '';
343 if ($acct && ($acct->{status} // '') eq 'connected') {
344 $actions_row = <<HTML;
345<div style="display:flex;gap:8px;margin-top:10px;border-top:1px dashed #1f2a4a;padding-top:10px">
346 <form method="POST" action="/marketplaces.cgi" style="display:inline">
347 <input type="hidden" name="action" value="test">
348 <input type="hidden" name="platform" value="$slug">
349 <button type="submit" class="btn btn-secondary btn-sm">Test connection</button>
350 </form>
351 <form method="POST" action="/marketplaces.cgi" style="display:inline"
352 onsubmit="return confirm('Disconnect $p->{name}? Your products will stay; this just revokes API access.');">
353 <input type="hidden" name="action" value="disconnect">
354 <input type="hidden" name="platform" value="$slug">
355 <button type="submit" class="btn btn-ghost btn-sm" style="color:#fca5a5">Disconnect</button>
356 </form>
357</div>
358HTML
359 }
360
361 my $err_html = $last_err ? qq~<div style="font-size:12px;color:#fca5a5;margin-top:6px">Last error: $last_err</div>~ : '';
362 my $handle_html = $handle ? qq~<div style="font-size:12px;color:#7e92b6;margin-top:6px">Linked account: <b style="color:#e6ecf6">$handle</b></div>~ : '';
363 my $last_used_html = $last_used ? qq~<div style="font-size:11px;color:#7e92b6;margin-top:4px">Last used: $last_used</div>~ : '';
364
365 # Multi-account list (only renders when there's > 1 account on this
366 # platform). Lets sellers see at a glance which of their multiple
367 # marketplace accounts is in which state.
368 my $multi_html = '';
369 if (@$accts > 1) {
370 $multi_html = '<div style="margin-top:10px;padding-top:10px;border-top:1px dashed #1f2a4a;font-size:12px;color:#7e92b6">';
371 $multi_html .= '<div style="text-transform:uppercase;letter-spacing:1px;font-size:10px;margin-bottom:6px">All ' . _h($p->{name}) . ' accounts</div>';
372 foreach my $a (@$accts) {
373 my $h_handle = _h($a->{account_handle} // '');
374 my $h_status = _h($a->{status} // '');
375 my $sc = $a->{status} eq 'connected' ? '#10b981' : ($a->{status} eq 'error' ? '#fca5a5' : '#7e92b6');
376 $multi_html .= qq~<div style="display:flex;justify-content:space-between;padding:4px 0">
377 <span style="font-family:ui-monospace,Menlo,monospace;color:#e6ecf6">$h_handle</span>
378 <span style="color:$sc;font-weight:600;font-size:11px">$h_status</span>
379 </div>~;
380 }
381 $multi_html .= '</div>';
382 }
383
384 my $icon = _h($p->{icon} // '?');
385 my $accent = _h($p->{accent} // '#14b8a6');
386 my $tag = _h($p->{tagline} // '');
387 my $name = _h($p->{name});
388 my $plan = _h($p->{seller_plan_required} // '');
389 my $signup = _h($p->{signup_url} // '');
390 my $docs = _h($p->{developer_url} // '');
391
392 $cards .= <<HTML;
393<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:18px 20px;margin-bottom:14px">
394 <div style="display:flex;align-items:center;gap:12px">
395 <div style="width:38px;height:38px;border-radius:9px;background:${accent}1a;color:$accent;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px">$icon</div>
396 <div style="flex:1">
397 <div style="font-size:18px;font-weight:600;color:#fff">$name</div>
398 <div style="font-size:13px;color:#7e92b6">$tag</div>
399 </div>
400 <span class="status-pill $status_class">$status_label</span>
401 </div>
402 <div style="font-size:12px;color:#7e92b6;margin-top:10px">Seller plan needed: $plan</div>
403 $handle_html
404 $last_used_html
405 $err_html
406 $multi_html
407 $controls
408 $actions_row
409 <div style="font-size:11px;color:#5f719b;margin-top:8px">
410 <a href="$signup" target="_blank" rel="noopener" style="color:#7e92b6">Sign up for $name &rarr;</a>
411 &middot;
412 <a href="$docs" target="_blank" rel="noopener" style="color:#7e92b6">$name API docs &rarr;</a>
413 </div>
414</section>
415HTML
416}
417
418my $flash_html = '';
419if (length $flash) {
420 my %color = (success => '#10b981', warn => '#fbbf24', error => '#fca5a5', info => '#10b981');
421 my $c = $color{$flash_kind} || $color{info};
422 $flash_html = qq~<div style="background:${c}1a;border-left:3px solid $c;color:$c;padding:12px 16px;border-radius:8px;margin-bottom:18px;font-size:14px">$flash</div>~;
423}
424
425my $body = <<HTML;
426<style>
427 .status-pill { font-size:11px; letter-spacing:1.5px; text-transform:uppercase; padding:3px 9px; border-radius:999px; font-weight:600; }
428 .status-pill.green { background:#10b98122; color:#10b981; }
429 .status-pill.amber { background:#fbbf2422; color:#fbbf24; }
430 .status-pill.red { background:#fca5a522; color:#fca5a5; }
431 .status-pill.gray { background:#33415522; color:#94a3b8; }
432 .btn-primary { background:#064e3b; color:#a7f3d0; border:0; padding:7px 14px; border-radius:6px; font-weight:600; cursor:pointer; }
433 .btn-primary:hover { background:#67e8f9; }
434 .btn-secondary { background:#1f2a4a; color:#e6ecf6; border:0; padding:7px 14px; border-radius:6px; cursor:pointer; }
435 .btn-ghost { background:transparent; color:#94a3b8; border:0; padding:7px 14px; border-radius:6px; cursor:pointer; }
436 .btn-sm { font-size:13px; }
437</style>
438
439<div style="max-width:820px;margin:0 auto;padding:24px 20px">
440 <div style="margin-bottom:18px">
441 <div style="font-size:11px;letter-spacing:2px;color:#14b8a6;text-transform:uppercase">Marketplace Integrations</div>
442 <h1 style="font-size:26px;color:#fff;margin:6px 0 4px;font-weight:700">Connect your marketplaces</h1>
443 <p style="color:#7e92b6;font-size:14px;margin:0">Authorise RePricer to read your competitor offers and update your prices. We use OAuth or API keys -- we never store your marketplace login.</p>
444 </div>
445
446 $flash_html
447 $cards
448
449 <div style="margin-top:24px;padding:16px 18px;background:#0a0f1f;border:1px dashed #1f2a4a;border-radius:10px;font-size:13px;color:#7e92b6">
450 <b style="color:#e6ecf6">How it works.</b>
451 For Amazon and eBay you'll click "Connect" and authorize RePricer on the marketplace's own consent screen.
452 For Walmart, paste your Consumer ID + Private Key from Seller Center -- Walmart doesn't use OAuth.
453 After connecting, head to <a href="/products.cgi" style="color:#14b8a6">Products</a> to import your SKUs, then set up
454 <a href="/repricing_rules.cgi" style="color:#14b8a6">repricing rules</a> to start winning the buy box.
455 </div>
456</div>
457HTML
458
459print "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";
460$wrap->render({
461 userinfo => $userinfo,
462 page_key => 'marketplaces',
463 title => 'Marketplaces',
464 body => $body,
465});
466exit;
467
468#======================================================================
469# oauth_amazon.cgi -- Amazon SP-API OAuth callback (ex-/oauth_amazon.cgi)
470#
471# Amazon redirects the seller back here after they grant consent in
472# Seller Central. We verify state, exchange the one-time code for an
473# LWA refresh token, upsert the marketplace_accounts row, and 302 back
474# to /marketplaces.cgi with a success/error flash.
475#======================================================================
476sub _oauth_amazon_cb {
477 require MODS::RePricer::Marketplaces::Amazon;
478
479 # User-cancel on the Amazon consent page comes back with error / error_description.
480 if ($form->{error} || $form->{error_description}) {
481 my $msg = $form->{error_description} || $form->{error} || 'Amazon consent cancelled';
482 print "Status: 302 Found\nLocation: /marketplaces.cgi?flash=" . _e($msg) . "&flash_kind=warn\n\n";
483 return;
484 }
485
486 my $code = $form->{spapi_oauth_code} || '';
487 my $state = $form->{state} || '';
488 my $sp_id = $form->{selling_partner_id} || '';
489
490 unless ($code && $sp_id) {
491 print "Status: 302 Found\nLocation: /marketplaces.cgi?flash="
492 . _e('Amazon callback missing spapi_oauth_code or selling_partner_id') . "&flash_kind=error\n\n";
493 return;
494 }
495
496 my $dbh = $db->db_connect();
497 my $row = $db->db_readwrite($dbh, qq~
498 SELECT oauth_state, oauth_marketplace_id FROM `${DB}`.users WHERE id='$uid' LIMIT 1
499 ~, $ENV{SCRIPT_NAME}, __LINE__);
500 my $expected_state = ($row && $row->{oauth_state}) || '';
501 my $marketplace_id = ($row && $row->{oauth_marketplace_id}) || 'ATVPDKIKX0DER';
502 unless (length $state && $state eq $expected_state) {
503 print "Status: 302 Found\nLocation: /marketplaces.cgi?flash="
504 . _e('OAuth state mismatch -- please try connecting again') . "&flash_kind=error\n\n";
505 return;
506 }
507
508 my $tok = MODS::RePricer::Marketplaces::Amazon->exchange_code_for_token($code);
509 unless ($tok->{ok}) {
510 print "Status: 302 Found\nLocation: /marketplaces.cgi?flash="
511 . _e('Amazon token exchange failed: ' . $tok->{error}) . "&flash_kind=error\n\n";
512 return;
513 }
514
515 my $refresh = $tok->{refresh_token}; $refresh =~ s/'/''/g;
516 my $access = $tok->{access_token} || ''; $access =~ s/'/''/g;
517 my $exp_at = $tok->{expires_in} ? (time() + $tok->{expires_in}) : 0;
518 my $sp_id_q = $sp_id; $sp_id_q =~ s/'/''/g;
519 my $meta_json = JSON::PP::encode_json({
520 seller_id => $sp_id,
521 marketplace_id => $marketplace_id,
522 access_token => $tok->{access_token},
523 access_token_expires_at=> $exp_at,
524 });
525 $meta_json =~ s/'/''/g;
526
527 my $existing = $db->db_readwrite($dbh, qq~
528 SELECT id FROM `${DB}`.marketplace_accounts
529 WHERE user_id='$uid' AND platform='amazon' LIMIT 1
530 ~, $ENV{SCRIPT_NAME}, __LINE__);
531 if ($existing && $existing->{id}) {
532 my $id = $existing->{id}; $id =~ s/[^0-9]//g;
533 $db->db_readwrite($dbh, qq~
534 UPDATE `${DB}`.marketplace_accounts
535 SET access_token='$refresh',
536 status='connected',
537 account_handle='$sp_id_q',
538 metadata='$meta_json',
539 last_error=NULL,
540 connected_at=COALESCE(connected_at, NOW()),
541 last_used_at=NOW(),
542 updated_at=NOW()
543 WHERE id='$id'
544 ~, $ENV{SCRIPT_NAME}, __LINE__);
545 } else {
546 $db->db_readwrite($dbh, qq~
547 INSERT INTO `${DB}`.marketplace_accounts
548 (user_id, platform, status, access_token, account_handle,
549 metadata, connected_at, last_used_at, created_at)
550 VALUES ('$uid', 'amazon', 'connected', '$refresh', '$sp_id_q',
551 '$meta_json', NOW(), NOW(), NOW())
552 ~, $ENV{SCRIPT_NAME}, __LINE__);
553 }
554
555 # Wipe the one-time CSRF state.
556 $db->db_readwrite($dbh, qq~
557 UPDATE `${DB}`.users SET oauth_state=NULL, oauth_marketplace_id=NULL WHERE id='$uid'
558 ~, $ENV{SCRIPT_NAME}, __LINE__);
559
560 print "Status: 302 Found\nLocation: /marketplaces.cgi?flash="
561 . _e("Amazon connected -- seller $sp_id") . "&flash_kind=success\n\n";
562}
563
564#======================================================================
565# oauth_ebay.cgi -- eBay OAuth2 callback (ex-/oauth_ebay.cgi)
566#
567# eBay redirects here after consent. We verify state, exchange the
568# code for { access_token, refresh_token }, upsert the row, and 302
569# back to /marketplaces.cgi.
570#======================================================================
571sub _oauth_ebay_cb {
572 require MODS::RePricer::Marketplaces::EBay;
573
574 if ($form->{error} || $form->{error_description}) {
575 my $msg = $form->{error_description} || $form->{error} || 'eBay consent cancelled';
576 print "Status: 302 Found\nLocation: /marketplaces.cgi?flash=" . _e($msg) . "&flash_kind=warn\n\n";
577 return;
578 }
579
580 my $code = $form->{code} || '';
581 my $state = $form->{state} || '';
582 unless ($code) {
583 print "Status: 302 Found\nLocation: /marketplaces.cgi?flash="
584 . _e('eBay callback missing code') . "&flash_kind=error\n\n";
585 return;
586 }
587
588 my $dbh = $db->db_connect();
589 my $row = $db->db_readwrite($dbh, qq~
590 SELECT oauth_state, oauth_marketplace_id FROM `${DB}`.users WHERE id='$uid' LIMIT 1
591 ~, $ENV{SCRIPT_NAME}, __LINE__);
592 my $expected_state = ($row && $row->{oauth_state}) || '';
593 my $marketplace_id = ($row && $row->{oauth_marketplace_id}) || 'EBAY_US';
594 unless (length $state && $state eq $expected_state) {
595 print "Status: 302 Found\nLocation: /marketplaces.cgi?flash="
596 . _e('OAuth state mismatch -- please try connecting again') . "&flash_kind=error\n\n";
597 return;
598 }
599
600 my $tok = MODS::RePricer::Marketplaces::EBay->exchange_code_for_token($code);
601 unless ($tok->{ok}) {
602 print "Status: 302 Found\nLocation: /marketplaces.cgi?flash="
603 . _e('eBay token exchange failed: ' . $tok->{error}) . "&flash_kind=error\n\n";
604 return;
605 }
606
607 my $refresh = $tok->{refresh_token}; $refresh =~ s/'/''/g;
608 my $access = $tok->{access_token} || '';
609 my $exp_at = $tok->{expires_in} ? (time() + $tok->{expires_in}) : 0;
610 my $meta_json = JSON::PP::encode_json({
611 marketplace_id => $marketplace_id,
612 access_token => $access,
613 access_token_expires_at => $exp_at,
614 });
615 $meta_json =~ s/'/''/g;
616
617 my $existing = $db->db_readwrite($dbh, qq~
618 SELECT id FROM `${DB}`.marketplace_accounts
619 WHERE user_id='$uid' AND platform='ebay' LIMIT 1
620 ~, $ENV{SCRIPT_NAME}, __LINE__);
621 if ($existing && $existing->{id}) {
622 my $id = $existing->{id}; $id =~ s/[^0-9]//g;
623 $db->db_readwrite($dbh, qq~
624 UPDATE `${DB}`.marketplace_accounts
625 SET refresh_token='$refresh',
626 status='connected',
627 metadata='$meta_json',
628 last_error=NULL,
629 connected_at=COALESCE(connected_at, NOW()),
630 last_used_at=NOW(),
631 updated_at=NOW()
632 WHERE id='$id'
633 ~, $ENV{SCRIPT_NAME}, __LINE__);
634 } else {
635 $db->db_readwrite($dbh, qq~
636 INSERT INTO `${DB}`.marketplace_accounts
637 (user_id, platform, status, refresh_token,
638 metadata, connected_at, last_used_at, created_at)
639 VALUES ('$uid', 'ebay', 'connected', '$refresh',
640 '$meta_json', NOW(), NOW(), NOW())
641 ~, $ENV{SCRIPT_NAME}, __LINE__);
642 }
643
644 $db->db_readwrite($dbh, qq~
645 UPDATE `${DB}`.users SET oauth_state=NULL, oauth_marketplace_id=NULL WHERE id='$uid'
646 ~, $ENV{SCRIPT_NAME}, __LINE__);
647
648 print "Status: 302 Found\nLocation: /marketplaces.cgi?flash="
649 . _e('eBay connected.') . "&flash_kind=success\n\n";
650}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help