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

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

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

Added
+445
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to be9f29deaa31
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 -- account / identity page.
4#
5# GET /profile.cgi
6#
7# POST act=update_profile display_name, email, timezone, default_currency
8# POST act=change_password old_password, new_password, confirm_password
9# POST act=resend_verification (re-mint + email a verification link)
10# POST act=update_notifications notify_floor_hit, notify_buybox_lost, notify_weekly_summary
11# POST act=update_slack slack_webhook_url
12# POST act=create_api_key label (Studio tier only)
13# POST act=revoke_api_key key_id
14#
15# 2FA enable/disable is in /two_factor_setup.cgi (separate flow).
16#
17# UI-prefs (notifications/theme/etc) live at /preferences.cgi -- this
18# file is the account-identity surface only. Renamed from the legacy
19# tabbed /settings.cgi 2026-06-20 to match the PTMatrix URL convention.
20#======================================================================
21use strict;
22use warnings;
23
24use lib '/var/www/vhosts/3dshawn.com/repricer.3dshawn.com';
25use CGI;
26use Digest::SHA qw(sha256_hex);
27use MODS::DBConnect;
28use MODS::Login;
29use MODS::RePricer::Config;
30use MODS::RePricer::Wrapper;
31
32$|=1;
33
34my $q = CGI->new;
35my $form = $q->Vars;
36my $auth = MODS::Login->new;
37my $wrap = MODS::RePricer::Wrapper->new;
38my $db = MODS::DBConnect->new;
39my $cfg = MODS::RePricer::Config->new;
40my $DB = $cfg->settings('database_name');
41
42my $userinfo = $auth->login_verify();
43unless ($userinfo) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; }
44my $uid = $userinfo->{user_id}; $uid =~ s/[^0-9]//g;
45
46sub _h { my $s = shift // ''; $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g; $s =~ s/"/&quot;/g; $s }
47sub _q { my $s = shift // ''; $s =~ s/'/''/g; $s }
48sub _e { my $s = shift // ''; $s =~ s/([^A-Za-z0-9\-._~])/sprintf('%%%02X', ord($1))/ge; $s }
49
50my $dbh = $db->db_connect();
51
52# ----- POST handlers -------------------------------------------------
53my $flash = ''; my $kind = '';
54if (($ENV{REQUEST_METHOD} || '') eq 'POST') {
55 my $act = $form->{act} || '';
56
57 if ($act eq 'update_profile') {
58 my $name = substr(_q($form->{display_name} // ''), 0, 80);
59 my $email = substr(_q($form->{email} // ''), 0, 191);
60 my $tz = substr(_q($form->{timezone} // ''), 0, 64);
61 my $cur = ($form->{default_currency} || '') =~ /^([A-Z]{3})$/ ? $1 : '';
62 unless ($email =~ /\@/) {
63 $flash = 'Email looks malformed.'; $kind = 'error';
64 } else {
65 $db->db_readwrite($dbh, qq~
66 UPDATE `${DB}`.users SET
67 display_name='$name',
68 email='$email',
69 timezone='$tz'
70 ~ . ($cur ? ", default_currency='$cur'" : "") . qq~
71 WHERE id='$uid'
72 ~, $ENV{SCRIPT_NAME}, __LINE__);
73 $flash = 'Profile updated.'; $kind = 'success';
74 }
75 }
76 elsif ($act eq 'change_password') {
77 my $old_pw = $form->{old_password} // '';
78 my $new_pw = $form->{new_password} // '';
79 my $cf_pw = $form->{confirm_password} // '';
80 if (length($new_pw) < 8) {
81 $flash = 'New password must be at least 8 characters.'; $kind = 'error';
82 } elsif ($new_pw ne $cf_pw) {
83 $flash = 'New password and confirmation do not match.'; $kind = 'error';
84 } else {
85 # Verify current password. Hashing: the existing scheme uses
86 # sha256_hex of password (matches what Login likely does).
87 # Adjust here if your Login module uses a different scheme.
88 my $row = $db->db_readwrite($dbh, qq~
89 SELECT password_hash FROM `${DB}`.users WHERE id='$uid'
90 ~, $ENV{SCRIPT_NAME}, __LINE__);
91 my $stored = $row->{password_hash} // '';
92 my $check = sha256_hex($old_pw);
93 if ($stored ne $check) {
94 $flash = 'Current password is wrong.'; $kind = 'error';
95 } else {
96 my $new_hash = sha256_hex($new_pw);
97 $db->db_readwrite($dbh, qq~
98 UPDATE `${DB}`.users SET password_hash='$new_hash' WHERE id='$uid'
99 ~, $ENV{SCRIPT_NAME}, __LINE__);
100 $flash = 'Password changed.'; $kind = 'success';
101 }
102 }
103 }
104 elsif ($act eq 'update_slack') {
105 my $url = $form->{slack_webhook_url} // '';
106 $url =~ s/^\s+|\s+$//g;
107 # Allow empty (clears) or a Slack-shaped URL only.
108 if ($url ne '' && $url !~ m{^https://hooks\.slack\.com/services/T[A-Z0-9]+/B[A-Z0-9]+/[A-Za-z0-9]+$}) {
109 $flash = 'Slack webhook URL must look like https://hooks.slack.com/services/T.../B.../...'; $kind = 'error';
110 } else {
111 my $qurl = _q($url);
112 $db->db_readwrite($dbh, qq~
113 UPDATE `${DB}`.users SET slack_webhook_url='$qurl' WHERE id='$uid'
114 ~, $ENV{SCRIPT_NAME}, __LINE__);
115 $flash = $url ? 'Slack webhook saved.' : 'Slack webhook cleared.'; $kind = 'success';
116 }
117 }
118 elsif ($act eq 'create_api_key') {
119 # Studio-tier gate -- enforce server-side, even though UI hides it.
120 my $trow = $db->db_readwrite($dbh, qq~
121 SELECT plan_tier FROM `${DB}`.users WHERE id='$uid'
122 ~, $ENV{SCRIPT_NAME}, __LINE__);
123 my $cur_tier = $trow->{plan_tier} || 'free';
124 if ($cur_tier ne 'studio') {
125 $flash = 'API access is a Studio-tier feature. Upgrade in /billing.cgi.'; $kind = 'error';
126 } else {
127 my $label = substr(_q($form->{label} // 'unnamed'), 0, 60);
128 # Token format: rpc_live_<48 hex>. We store the sha256 hash;
129 # the plain token is shown ONCE in the flash.
130 my @chars = ('a'..'f', 0..9);
131 my $plain = 'rpc_live_' . join('', map { $chars[int(rand(@chars))] } 1..48);
132 my $h = sha256_hex($plain);
133 # key_prefix gives the user something to recognize their key by
134 # in lists; we never display the full token after creation.
135 my $prefix = substr($plain, 0, 8);
136 $db->db_readwrite($dbh, qq~
137 INSERT INTO `${DB}`.api_keys (user_id, label, key_prefix, key_hash, created_at)
138 VALUES ('$uid', '$label', '$prefix', '$h', NOW())
139 ~, $ENV{SCRIPT_NAME}, __LINE__);
140 $flash = "API key created. Copy it now -- it will not be shown again:|$plain";
141 $kind = 'success';
142 }
143 }
144 elsif ($act eq 'revoke_api_key') {
145 my $kid = $form->{key_id} || ''; $kid =~ s/[^0-9]//g;
146 if ($kid) {
147 $db->db_readwrite($dbh, qq~
148 UPDATE `${DB}`.api_keys SET revoked_at=NOW()
149 WHERE id='$kid' AND user_id='$uid' AND revoked_at IS NULL
150 ~, $ENV{SCRIPT_NAME}, __LINE__);
151 $flash = 'API key revoked.'; $kind = 'success';
152 }
153 }
154 elsif ($act eq 'update_notifications') {
155 my $a = $form->{notify_floor_hit} ? 1 : 0;
156 my $b = $form->{notify_buybox_lost} ? 1 : 0;
157 my $c = $form->{notify_weekly_summary} ? 1 : 0;
158 $db->db_readwrite($dbh, qq~
159 UPDATE `${DB}`.users SET
160 notify_floor_hit='$a',
161 notify_buybox_lost='$b',
162 notify_weekly_summary='$c'
163 WHERE id='$uid'
164 ~, $ENV{SCRIPT_NAME}, __LINE__);
165 $flash = 'Notification preferences saved.'; $kind = 'success';
166 }
167 elsif ($act eq 'resend_verification') {
168 # Re-mint a verification token and email the link. We require
169 # the account to NOT already be verified -- there's no reason
170 # to ship another email after the user already confirmed.
171 # (Migrated from the legacy settings.cgi 2026-06-20.)
172 require MODS::RePricer::EmailVerify;
173 my $u_check = $db->db_readwrite($dbh, qq~
174 SELECT id, email, display_name, email_verified_at
175 FROM `${DB}`.users WHERE id='$uid' LIMIT 1
176 ~, $ENV{SCRIPT_NAME}, __LINE__);
177 if ($u_check && $u_check->{id} && !$u_check->{email_verified_at}) {
178 my $r = MODS::RePricer::EmailVerify->send_for_user(
179 $db, $dbh, $DB,
180 { id => $u_check->{id}, email => $u_check->{email}, display_name => $u_check->{display_name} },
181 );
182 if ($r && $r->{ok}) {
183 $flash = 'Verification email sent. Check your inbox (and spam folder).';
184 $kind = 'success';
185 } else {
186 $flash = 'Could not send verification email: ' . ($r->{error} || 'unknown error');
187 $kind = 'error';
188 }
189 } else {
190 $flash = 'Your email is already verified.';
191 $kind = 'success';
192 }
193 }
194}
195
196# ----- Load current row ----------------------------------------------
197my $u = $db->db_readwrite($dbh, qq~
198 SELECT email, display_name, timezone, default_currency,
199 two_factor_enabled,
200 email_verified_at,
201 DATE_FORMAT(email_verified_at, '%b %e, %Y') AS verified_on,
202 notify_floor_hit, notify_buybox_lost, notify_weekly_summary,
203 slack_webhook_url, plan_tier,
204 created_at, last_login_at
205 FROM `${DB}`.users WHERE id='$uid'
206~, $ENV{SCRIPT_NAME}, __LINE__);
207
208my $api_keys = [];
209my $tier = $u->{plan_tier} || 'free';
210if ($tier eq 'studio') {
211 $api_keys = $db->db_readwrite_multiple($dbh, qq~
212 SELECT id, label, created_at, last_used_at, revoked_at
213 FROM `${DB}`.api_keys
214 WHERE user_id='$uid'
215 ORDER BY id DESC
216 ~, $ENV{SCRIPT_NAME}, __LINE__);
217}
218
219$db->db_disconnect($dbh);
220
221my $name = _h($u->{display_name} // '');
222my $email = _h($u->{email} // '');
223my $tz = _h($u->{timezone} || 'UTC');
224my $cur = _h($u->{default_currency} || 'USD');
225my $tfa = $u->{two_factor_enabled} ? 1 : 0;
226my $created = _h($u->{created_at} // '');
227my $last_login = _h($u->{last_login_at} // 'never');
228my $email_verified = $u->{email_verified_at} ? 1 : 0;
229my $verified_label = $email_verified
230 ? ('Verified ' . _h($u->{verified_on} || ''))
231 : 'Not verified yet';
232my $nf = $u->{notify_floor_hit} ? 'checked' : '';
233my $nbb = $u->{notify_buybox_lost} ? 'checked' : '';
234my $nws = $u->{notify_weekly_summary} ? 'checked' : '';
235my $slack_url = _h($u->{slack_webhook_url} // '');
236
237# ----- Render --------------------------------------------------------
238my $body = '<div style="max-width:780px;margin:0 auto;padding:24px 28px">';
239
240# Header
241$body .= '<div style="margin-bottom:24px">';
242$body .= ' <div style="font-size:11px;letter-spacing:2px;color:#14b8a6;text-transform:uppercase">Account</div>';
243$body .= ' <h1 style="font-size:26px;color:#fff;margin:6px 0 4px;font-weight:700">Profile &amp; preferences</h1>';
244$body .= ' <p style="color:#7e92b6;font-size:14px;margin:0">Account created ' . $created . ' &middot; last login ' . $last_login . '</p>';
245$body .= '</div>';
246
247if (length $flash) {
248 my $color = $kind eq 'success' ? '#10b981' : ($kind eq 'error' ? '#fca5a5' : '#10b981');
249 # Optional reveal-once payload separated by '|' (used by create_api_key)
250 my ($msg_raw, $reveal) = split /\|/, $flash, 2;
251 $body .= qq~<div style="background:${color}1a;border-left:3px solid $color;color:$color;padding:12px 16px;border-radius:8px;margin-bottom:8px;font-size:14px">~ . _h($msg_raw) . qq~</div>~;
252 if ($reveal) {
253 $body .= '<div style="background:#0b1226;border:2px solid #fbbf24;color:#fbbf24;padding:14px 18px;border-radius:8px;margin-bottom:18px;font-family:ui-monospace,Menlo,monospace;font-size:14px;word-break:break-all">' . _h($reveal) . '</div>';
254 }
255}
256
257# Profile section
258$body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:18px 22px;margin-bottom:16px">';
259$body .= ' <div style="font-size:11px;letter-spacing:1.5px;color:#7e92b6;text-transform:uppercase;font-weight:600;margin-bottom:14px">Profile</div>';
260$body .= qq~ <form method="POST" style="display:flex;flex-direction:column;gap:12px">
261 <input type="hidden" name="act" value="update_profile">
262 <label style="display:flex;flex-direction:column;gap:4px">
263 <span style="color:#7e92b6;font-size:12px">Display name</span>
264 <input type="text" name="display_name" value="$name" style="padding:8px 10px;background:#0a0f1f;border:1px solid #1f2a4a;color:#e6ecf6;border-radius:6px">
265 </label>
266 <label style="display:flex;flex-direction:column;gap:4px">
267 <span style="color:#7e92b6;font-size:12px">Email</span>
268 <input type="email" name="email" value="$email" style="padding:8px 10px;background:#0a0f1f;border:1px solid #1f2a4a;color:#e6ecf6;border-radius:6px">
269 </label>
270 <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
271 <label style="display:flex;flex-direction:column;gap:4px">
272 <span style="color:#7e92b6;font-size:12px">Timezone (IANA, e.g. America/New_York)</span>
273 <input type="text" name="timezone" value="$tz" style="padding:8px 10px;background:#0a0f1f;border:1px solid #1f2a4a;color:#e6ecf6;border-radius:6px">
274 </label>
275 <label style="display:flex;flex-direction:column;gap:4px">
276 <span style="color:#7e92b6;font-size:12px">Default currency</span>
277 <input type="text" name="default_currency" value="$cur" maxlength="3" style="padding:8px 10px;background:#0a0f1f;border:1px solid #1f2a4a;color:#e6ecf6;border-radius:6px;text-transform:uppercase">
278 </label>
279 </div>
280 <div style="text-align:right">
281 <button type="submit" style="background:#064e3b;color:#a7f3d0;border:0;padding:8px 18px;border-radius:6px;font-weight:600;cursor:pointer">Save profile</button>
282 </div>
283 </form>~;
284$body .= '</section>';
285
286# Password section
287$body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:18px 22px;margin-bottom:16px">';
288$body .= ' <div style="font-size:11px;letter-spacing:1.5px;color:#7e92b6;text-transform:uppercase;font-weight:600;margin-bottom:14px">Change password</div>';
289$body .= ' <form method="POST" style="display:flex;flex-direction:column;gap:12px">
290 <input type="hidden" name="act" value="change_password">
291 <label style="display:flex;flex-direction:column;gap:4px">
292 <span style="color:#7e92b6;font-size:12px">Current password</span>
293 <input type="password" name="old_password" autocomplete="current-password" style="padding:8px 10px;background:#0a0f1f;border:1px solid #1f2a4a;color:#e6ecf6;border-radius:6px">
294 </label>
295 <label style="display:flex;flex-direction:column;gap:4px">
296 <span style="color:#7e92b6;font-size:12px">New password (min 8 characters)</span>
297 <input type="password" name="new_password" autocomplete="new-password" style="padding:8px 10px;background:#0a0f1f;border:1px solid #1f2a4a;color:#e6ecf6;border-radius:6px">
298 </label>
299 <label style="display:flex;flex-direction:column;gap:4px">
300 <span style="color:#7e92b6;font-size:12px">Confirm new password</span>
301 <input type="password" name="confirm_password" autocomplete="new-password" style="padding:8px 10px;background:#0a0f1f;border:1px solid #1f2a4a;color:#e6ecf6;border-radius:6px">
302 </label>
303 <div style="text-align:right">
304 <button type="submit" style="background:#064e3b;color:#a7f3d0;border:0;padding:8px 18px;border-radius:6px;font-weight:600;cursor:pointer">Change password</button>
305 </div>
306 </form>';
307$body .= '</section>';
308
309# Email verification section (merged in from legacy settings.cgi 2026-06-20)
310$body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:18px 22px;margin-bottom:16px">';
311$body .= ' <div style="font-size:11px;letter-spacing:1.5px;color:#7e92b6;text-transform:uppercase;font-weight:600;margin-bottom:8px">Email verification</div>';
312if ($email_verified) {
313 $body .= ' <div style="color:#10b981;font-size:14px;font-weight:600">&check; Email verified</div>';
314 $body .= ' <div style="color:#7e92b6;font-size:13px;margin-top:6px">' . $verified_label . '.</div>';
315} else {
316 $body .= ' <div style="color:#fbbf24;font-size:14px;font-weight:600">Email not verified yet</div>';
317 $body .= ' <div style="color:#7e92b6;font-size:13px;margin-top:6px">Verify your address so password resets and important account notices reach you reliably.</div>';
318 $body .= ' <form method="POST" style="margin-top:10px">';
319 $body .= ' <input type="hidden" name="act" value="resend_verification">';
320 $body .= ' <button type="submit" style="background:#064e3b;color:#a7f3d0;border:0;padding:8px 14px;border-radius:6px;font-weight:600;cursor:pointer;font-size:13px">Send verification email</button>';
321 $body .= ' </form>';
322}
323$body .= '</section>';
324
325# 2FA section -- link out to dedicated CGI
326$body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:18px 22px;margin-bottom:16px">';
327$body .= ' <div style="font-size:11px;letter-spacing:1.5px;color:#7e92b6;text-transform:uppercase;font-weight:600;margin-bottom:8px">Two-factor authentication</div>';
328if ($tfa) {
329 $body .= ' <div style="color:#10b981;font-size:14px;font-weight:600">&check; 2FA is ON</div>';
330 $body .= ' <div style="color:#7e92b6;font-size:13px;margin-top:6px">An authenticator code is required on every sign-in.</div>';
331 $body .= ' <div style="margin-top:10px"><a href="/two_factor_setup.cgi" style="color:#14b8a6;font-size:13px">Manage 2FA &rarr;</a></div>';
332} else {
333 $body .= ' <div style="color:#fbbf24;font-size:14px;font-weight:600">2FA is OFF</div>';
334 $body .= ' <div style="color:#7e92b6;font-size:13px;margin-top:6px">Add a second factor to protect your account from password compromise. Works with Google Authenticator, Authy, 1Password, and any TOTP-compatible app.</div>';
335 $body .= ' <div style="margin-top:10px"><a href="/two_factor_setup.cgi" style="background:#064e3b;color:#a7f3d0;padding:8px 14px;border-radius:6px;text-decoration:none;font-weight:600;font-size:13px">Enable 2FA</a></div>';
336}
337$body .= '</section>';
338
339# Notification prefs
340$body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:18px 22px;margin-bottom:16px">';
341$body .= ' <div style="font-size:11px;letter-spacing:1.5px;color:#7e92b6;text-transform:uppercase;font-weight:600;margin-bottom:14px">Email notifications</div>';
342$body .= qq~ <form method="POST" style="display:flex;flex-direction:column;gap:10px">
343 <input type="hidden" name="act" value="update_notifications">
344 <label style="display:flex;align-items:center;gap:10px;cursor:pointer">
345 <input type="checkbox" name="notify_floor_hit" value="1" $nf>
346 <div>
347 <div style="color:#e6ecf6;font-size:14px">Floor-hit alerts</div>
348 <div style="color:#7e92b6;font-size:12px">Email when a product's repricer wanted to go below its floor.</div>
349 </div>
350 </label>
351 <label style="display:flex;align-items:center;gap:10px;cursor:pointer">
352 <input type="checkbox" name="notify_buybox_lost" value="1" $nbb>
353 <div>
354 <div style="color:#e6ecf6;font-size:14px">Buy Box lost</div>
355 <div style="color:#7e92b6;font-size:12px">Email when a product you were holding the Buy Box on loses it.</div>
356 </div>
357 </label>
358 <label style="display:flex;align-items:center;gap:10px;cursor:pointer">
359 <input type="checkbox" name="notify_weekly_summary" value="1" $nws>
360 <div>
361 <div style="color:#e6ecf6;font-size:14px">Weekly summary</div>
362 <div style="color:#7e92b6;font-size:12px">Every Monday: Buy Box win rate, reprice count, top winners + losers.</div>
363 </div>
364 </label>
365 <div style="text-align:right">
366 <button type="submit" style="background:#064e3b;color:#a7f3d0;border:0;padding:8px 18px;border-radius:6px;font-weight:600;cursor:pointer">Save preferences</button>
367 </div>
368 </form>~;
369$body .= '</section>';
370
371# Slack webhook section
372$body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:18px 22px;margin-bottom:16px">';
373$body .= ' <div style="font-size:11px;letter-spacing:1.5px;color:#7e92b6;text-transform:uppercase;font-weight:600;margin-bottom:14px">Slack alerts (optional)</div>';
374$body .= qq~ <form method="POST" style="display:flex;flex-direction:column;gap:10px">
375 <input type="hidden" name="act" value="update_slack">
376 <label style="display:flex;flex-direction:column;gap:4px">
377 <span style="color:#7e92b6;font-size:12px">Incoming webhook URL</span>
378 <input type="url" name="slack_webhook_url" value="$slack_url" placeholder="https://hooks.slack.com/services/T.../B.../..." style="padding:8px 10px;background:#0a0f1f;border:1px solid #1f2a4a;color:#e6ecf6;border-radius:6px;font-family:ui-monospace,Menlo,monospace;font-size:12px">
379 </label>
380 <div style="color:#7e92b6;font-size:12px">When enabled, the events checked above also post to Slack. Create an incoming webhook in your Slack workspace and paste the URL here. Leave blank to disable.</div>
381 <div style="text-align:right">
382 <button type="submit" style="background:#064e3b;color:#a7f3d0;border:0;padding:8px 18px;border-radius:6px;font-weight:600;cursor:pointer">Save webhook</button>
383 </div>
384 </form>~;
385$body .= '</section>';
386
387# API keys section -- Studio tier only
388$body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:18px 22px;margin-bottom:16px">';
389$body .= ' <div style="font-size:11px;letter-spacing:1.5px;color:#7e92b6;text-transform:uppercase;font-weight:600;margin-bottom:6px">REST API access</div>';
390if ($tier ne 'studio') {
391 $body .= ' <div style="color:#fbbf24;font-size:13px;margin-bottom:6px">Studio plan feature</div>';
392 $body .= ' <div style="color:#7e92b6;font-size:13px">Get programmatic access to products, rules, and price changes via /api/v1. <a href="/billing.cgi" style="color:#14b8a6">Upgrade to Studio</a> to enable.</div>';
393} else {
394 $body .= ' <div style="color:#7e92b6;font-size:13px;margin-bottom:14px">Use these tokens as <code style="color:#14b8a6">Authorization: Bearer rpc_live_&hellip;</code> against <code style="color:#14b8a6">/api/v1/&hellip;</code>. Endpoints: <code>me</code>, <code>products</code>, <code>products/N</code>, <code>rules</code>, <code>price_changes</code>.</div>';
395 $body .= ' <form method="POST" style="display:flex;gap:8px;margin-bottom:14px">';
396 $body .= ' <input type="hidden" name="act" value="create_api_key">';
397 $body .= ' <input type="text" name="label" placeholder="Label (e.g. zapier, my-laptop)" maxlength="60" style="flex:1;padding:8px 10px;background:#0a0f1f;border:1px solid #1f2a4a;color:#e6ecf6;border-radius:6px">';
398 $body .= ' <button type="submit" style="background:#064e3b;color:#a7f3d0;border:0;padding:8px 16px;border-radius:6px;font-weight:600;cursor:pointer">Create key</button>';
399 $body .= ' </form>';
400 if (@$api_keys) {
401 $body .= ' <table style="width:100%;font-size:13px;border-collapse:collapse"><thead><tr style="color:#7e92b6;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:1px"><th style="padding:6px 4px">Label</th><th style="padding:6px 4px">Created</th><th style="padding:6px 4px">Last used</th><th style="padding:6px 4px">Status</th><th></th></tr></thead><tbody>';
402 foreach my $k (@$api_keys) {
403 my $st = $k->{revoked_at} ? '<span style="color:#fca5a5">revoked</span>' : '<span style="color:#10b981">active</span>';
404 my $lu = $k->{last_used_at} || '<span style="color:#7e92b6">never</span>';
405 $body .= '<tr style="border-top:1px solid #1f2a4a">';
406 $body .= '<td style="padding:6px 4px;color:#e6ecf6">' . _h($k->{label}) . '</td>';
407 $body .= '<td style="padding:6px 4px;color:#7e92b6">' . _h($k->{created_at}) . '</td>';
408 $body .= '<td style="padding:6px 4px;color:#7e92b6">' . $lu . '</td>';
409 $body .= '<td style="padding:6px 4px">' . $st . '</td>';
410 if (!$k->{revoked_at}) {
411 $body .= '<td style="padding:6px 4px;text-align:right"><form method="POST" style="display:inline" onsubmit="return confirm(\'Revoke this API key? Any integration using it will stop working.\')"><input type="hidden" name="act" value="revoke_api_key"><input type="hidden" name="key_id" value="' . $k->{id} . '"><button type="submit" style="background:transparent;color:#fca5a5;border:0;cursor:pointer;font-size:12px">Revoke</button></form></td>';
412 } else {
413 $body .= '<td></td>';
414 }
415 $body .= '</tr>';
416 }
417 $body .= '</tbody></table>';
418 } else {
419 $body .= '<div style="color:#7e92b6;font-size:13px;font-style:italic">No keys yet.</div>';
420 }
421}
422$body .= '</section>';
423
424# Footer: links to other account-management pages
425$body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;padding:18px 22px;margin-bottom:16px">';
426$body .= ' <div style="font-size:11px;letter-spacing:1.5px;color:#7e92b6;text-transform:uppercase;font-weight:600;margin-bottom:10px">More account controls</div>';
427$body .= ' <div style="display:flex;flex-wrap:wrap;gap:10px;font-size:13px">';
428$body .= ' <a href="/sessions.cgi" style="color:#14b8a6;background:#1f2a4a;padding:8px 14px;border-radius:6px;text-decoration:none">Active sessions</a>';
429$body .= ' <a href="/account_export.cgi" style="color:#14b8a6;background:#1f2a4a;padding:8px 14px;border-radius:6px;text-decoration:none">Download my data</a>';
430$body .= ' <a href="/tax_settings.cgi" style="color:#14b8a6;background:#1f2a4a;padding:8px 14px;border-radius:6px;text-decoration:none">Tax / VAT settings</a>';
431$body .= ' <a href="/billing.cgi" style="color:#14b8a6;background:#1f2a4a;padding:8px 14px;border-radius:6px;text-decoration:none">Billing &amp; plan</a>';
432$body .= ' <a href="/account_close.cgi" style="color:#fca5a5;background:#1f2a4a;padding:8px 14px;border-radius:6px;text-decoration:none">Close my account</a>';
433$body .= ' </div>';
434$body .= '</section>';
435
436$body .= '</div>';
437
438print "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";
439$wrap->render({
440 userinfo => $userinfo,
441 page_key => 'profile',
442 title => 'Profile',
443 body => $body,
444});
445exit;
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help