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

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

added on local at 2026-07-01 21:46:56

Added
+195
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 5df9f43bb801
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 -- REST API dispatcher.
4#
5# Auth: Authorization: Bearer rpc_live_<48 hex>
6# Format: JSON in / JSON out, UTF-8.
7#
8# Routing (path comes from PATH_INFO):
9# GET /api/v1/me
10# GET /api/v1/products?limit=100&offset=0&marketplace=amazon
11# GET /api/v1/products/123
12# PATCH /api/v1/products/123 { reprice_enabled: 0|1, min_price_cents: N, max_price_cents: N }
13# GET /api/v1/rules
14# GET /api/v1/price_changes?limit=200
15#
16# Studio-tier only. Bearer tokens come from /profile.cgi.
17#======================================================================
18use strict;
19use warnings;
20use lib '/var/www/vhosts/3dshawn.com/repricer.3dshawn.com';
21use CGI;
22use JSON::PP;
23use Digest::SHA qw(sha256_hex);
24use MODS::DBConnect;
25use MODS::RePricer::Config;
26
27$|=1;
28
29my $q = CGI->new;
30my $db = MODS::DBConnect->new;
31my $cfg = MODS::RePricer::Config->new;
32my $DB = $cfg->settings('database_name');
33
34sub send_json {
35 my ($status, $payload) = @_;
36 my $body = JSON::PP->new->utf8->canonical->encode($payload);
37 print "Status: $status\n";
38 print "Content-Type: application/json; charset=utf-8\n";
39 print "Cache-Control: no-store\n";
40 print "Access-Control-Allow-Origin: *\n";
41 print "\n";
42 print $body;
43 exit;
44}
45
46sub err { send_json($_[0], { error => $_[1] }); }
47
48# ---- auth: Authorization header --------------------------------------
49my $auth_hdr = $ENV{HTTP_AUTHORIZATION} || '';
50my ($bearer) = $auth_hdr =~ /^Bearer\s+(rpc_live_[a-f0-9]{48})\s*$/;
51err(401, 'missing or malformed Authorization header') unless $bearer;
52
53my $h = sha256_hex($bearer);
54my $dbh = $db->db_connect();
55my $key = $db->db_readwrite($dbh, qq~
56 SELECT k.id AS key_id, k.user_id, u.plan_tier, u.account_status
57 FROM `${DB}`.api_keys k
58 JOIN `${DB}`.users u ON u.id = k.user_id
59 WHERE k.key_hash='$h' AND k.revoked_at IS NULL
60 LIMIT 1
61~, $ENV{SCRIPT_NAME}, __LINE__);
62
63err(401, 'invalid api key') unless $key && $key->{key_id};
64err(403, 'studio tier required') unless ($key->{plan_tier} || '') eq 'studio';
65err(403, 'account not active') unless ($key->{account_status} || 'active') eq 'active';
66
67my $uid = $key->{user_id}; $uid =~ s/[^0-9]//g;
68my $key_id = $key->{key_id}; $key_id =~ s/[^0-9]//g;
69
70# Stamp last_used_at -- fire-and-forget.
71$db->db_readwrite($dbh, qq~
72 UPDATE `${DB}`.api_keys SET last_used_at=NOW() WHERE id='$key_id'
73~, $ENV{SCRIPT_NAME}, __LINE__);
74
75# ---- route ----------------------------------------------------------
76my $path = $ENV{PATH_INFO} || '';
77$path =~ s{^/+}{}; # strip leading slashes
78$path =~ s{/+$}{}; # strip trailing slashes
79$path =~ s{^api/v1/?}{}; # strip /api/v1 prefix if present
80my @seg = split m{/}, $path;
81
82my $method = $ENV{REQUEST_METHOD} || 'GET';
83
84sub _q { my $s = shift // ''; $s =~ s/'/''/g; $s }
85
86# helper: safe integer arg
87sub _int { my $v = shift // 0; $v =~ s/[^0-9]//g; $v eq '' ? 0 : int($v) }
88
89# helper: read JSON body
90sub _read_body {
91 my $len = $ENV{CONTENT_LENGTH} || 0;
92 return {} unless $len > 0 && $len < 1_000_000;
93 my $raw; read(STDIN, $raw, $len);
94 my $j = eval { JSON::PP->new->utf8->decode($raw) };
95 return ref($j) eq 'HASH' ? $j : {};
96}
97
98# ---- GET /api/v1/me ------------------------------------------------
99if (@seg == 1 && $seg[0] eq 'me' && $method eq 'GET') {
100 my $u = $db->db_readwrite($dbh, qq~
101 SELECT id, email, display_name, plan_tier, account_status,
102 default_currency, timezone, created_at
103 FROM `${DB}`.users WHERE id='$uid'
104 ~, $ENV{SCRIPT_NAME}, __LINE__);
105 send_json(200, { user => $u });
106}
107
108# ---- GET /api/v1/products ------------------------------------------
109elsif (@seg == 1 && $seg[0] eq 'products' && $method eq 'GET') {
110 my $f = $q->Vars;
111 my $limit = _int($f->{limit} || 100); $limit = 500 if $limit > 500; $limit = 1 if $limit < 1;
112 my $offset = _int($f->{offset} || 0);
113 my $where = "user_id='$uid'";
114 if (my $mp = $f->{marketplace}) {
115 $mp =~ s/[^a-z]//g;
116 $where .= " AND marketplace='$mp'" if $mp;
117 }
118 my $rows = $db->db_readwrite_multiple($dbh, qq~
119 SELECT id, marketplace, sku, asin, walmart_item_id, title,
120 current_price_cents, cost_cents, min_price_cents,
121 max_price_cents, map_cents, inventory_qty,
122 reprice_enabled, status, created_at
123 FROM `${DB}`.products
124 WHERE $where
125 ORDER BY id DESC
126 LIMIT $limit OFFSET $offset
127 ~, $ENV{SCRIPT_NAME}, __LINE__);
128 send_json(200, { products => $rows, count => scalar(@$rows), limit => $limit, offset => $offset });
129}
130
131# ---- GET /api/v1/products/N ----------------------------------------
132elsif (@seg == 2 && $seg[0] eq 'products' && $method eq 'GET') {
133 my $pid = _int($seg[1]);
134 my $row = $db->db_readwrite($dbh, qq~
135 SELECT * FROM `${DB}`.products WHERE id='$pid' AND user_id='$uid'
136 ~, $ENV{SCRIPT_NAME}, __LINE__);
137 err(404, 'not found') unless $row && $row->{id};
138 send_json(200, { product => $row });
139}
140
141# ---- PATCH /api/v1/products/N --------------------------------------
142elsif (@seg == 2 && $seg[0] eq 'products' && ($method eq 'PATCH' || $method eq 'POST')) {
143 my $pid = _int($seg[1]);
144 my $own = $db->db_readwrite($dbh, qq~
145 SELECT id FROM `${DB}`.products WHERE id='$pid' AND user_id='$uid'
146 ~, $ENV{SCRIPT_NAME}, __LINE__);
147 err(404, 'not found') unless $own && $own->{id};
148 my $b = _read_body();
149 my @set;
150 if (exists $b->{reprice_enabled}) {
151 push @set, sprintf("reprice_enabled='%d'", $b->{reprice_enabled} ? 1 : 0);
152 }
153 foreach my $col (qw(min_price_cents max_price_cents cost_cents map_cents inventory_qty)) {
154 if (exists $b->{$col}) {
155 push @set, sprintf("%s='%d'", $col, _int($b->{$col}));
156 }
157 }
158 err(400, 'no updatable fields supplied') unless @set;
159 my $set = join(', ', @set);
160 $db->db_readwrite($dbh, qq~
161 UPDATE `${DB}`.products SET $set WHERE id='$pid' AND user_id='$uid'
162 ~, $ENV{SCRIPT_NAME}, __LINE__);
163 my $row = $db->db_readwrite($dbh, qq~
164 SELECT * FROM `${DB}`.products WHERE id='$pid'
165 ~, $ENV{SCRIPT_NAME}, __LINE__);
166 send_json(200, { product => $row });
167}
168
169# ---- GET /api/v1/rules ---------------------------------------------
170elsif (@seg == 1 && $seg[0] eq 'rules' && $method eq 'GET') {
171 my $rows = $db->db_readwrite_multiple($dbh, qq~
172 SELECT * FROM `${DB}`.repricing_rules WHERE user_id='$uid' ORDER BY id DESC
173 ~, $ENV{SCRIPT_NAME}, __LINE__);
174 send_json(200, { rules => $rows, count => scalar(@$rows) });
175}
176
177# ---- GET /api/v1/price_changes -------------------------------------
178elsif (@seg == 1 && $seg[0] eq 'price_changes' && $method eq 'GET') {
179 my $f = $q->Vars;
180 my $limit = _int($f->{limit} || 200); $limit = 1000 if $limit > 1000; $limit = 1 if $limit < 1;
181 my $rows = $db->db_readwrite_multiple($dbh, qq~
182 SELECT id, product_id, old_price_cents, new_price_cents, reason,
183 sync_status, automated, created_at
184 FROM `${DB}`.price_changes
185 WHERE user_id='$uid'
186 ORDER BY id DESC
187 LIMIT $limit
188 ~, $ENV{SCRIPT_NAME}, __LINE__);
189 send_json(200, { price_changes => $rows, count => scalar(@$rows), limit => $limit });
190}
191
192# ---- fallthrough ---------------------------------------------------
193else {
194 err(404, "no route: $method /$path");
195}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help