added on local at 2026-07-01 15:02:23
| 1 | #!/usr/bin/perl | |
| 2 | #====================================================================== | |
| 3 | # ContactForge - API Keys management. | |
| 4 | #====================================================================== | |
| 5 | use strict; | |
| 6 | use warnings; | |
| 7 | ||
| 8 | use lib '/var/www/vhosts/3dshawn.com/crm.3dshawn.com'; | |
| 9 | use CGI; | |
| 10 | use MODS::DBConnect; | |
| 11 | use MODS::Login; | |
| 12 | use MODS::ContactForge::Config; | |
| 13 | use MODS::ContactForge::Wrapper; | |
| 14 | use Digest::SHA qw(sha256_hex); | |
| 15 | ||
| 16 | my $q = CGI->new; | |
| 17 | my $form = $q->Vars; | |
| 18 | my $auth = MODS::Login->new; | |
| 19 | my $wrap = MODS::ContactForge::Wrapper->new; | |
| 20 | my $db = MODS::DBConnect->new; | |
| 21 | my $cfg = MODS::ContactForge::Config->new; | |
| 22 | my $DB = $cfg->settings('database_name'); | |
| 23 | $|=1; | |
| 24 | my $userinfo = $auth->login_verify(); | |
| 25 | if (!$userinfo) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; } | |
| 26 | my $uid = $userinfo->{user_id}; $uid =~ s/[^0-9]//g; | |
| 27 | ||
| 28 | my $dbh = $db->db_connect(); | |
| 29 | my $flash = ''; | |
| 30 | ||
| 31 | # Create | |
| 32 | if (($ENV{REQUEST_METHOD} || '') eq 'POST' && ($form->{op} || '') eq 'create') { | |
| 33 | my $name = $form->{name} || 'Unnamed key'; $name =~ s/'/''/g; $name = substr($name, 0, 120); | |
| 34 | # Generate token: cf_ + 40 hex chars | |
| 35 | my @c = (0..9, 'a'..'f'); | |
| 36 | my $rand = ''; $rand .= $c[int(rand(scalar @c))] for 1..40; | |
| 37 | my $token = "cf_$rand"; | |
| 38 | my $hash = sha256_hex($token); | |
| 39 | my $prefix = substr($token, 0, 12); | |
| 40 | $db->db_readwrite($dbh, qq~ | |
| 41 | INSERT INTO `${DB}`.api_keys | |
| 42 | SET user_id='$uid', name='$name', token_hash='$hash', | |
| 43 | token_prefix='$prefix', created_at=NOW() | |
| 44 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 45 | $flash = qq~Your new API key (copy now -- you won't see it again):<br><code style="background:#0b0f1a;padding:6px 10px;border-radius:4px;color:#67e8f9;font-size:13px;display:inline-block;margin-top:6px">$token</code>~; | |
| 46 | } | |
| 47 | ||
| 48 | # Revoke | |
| 49 | if (($ENV{REQUEST_METHOD} || '') eq 'POST' && ($form->{op} || '') eq 'revoke') { | |
| 50 | my $kid = $form->{id} || 0; $kid =~ s/[^0-9]//g; | |
| 51 | if ($kid) { | |
| 52 | $db->db_readwrite($dbh, qq~ | |
| 53 | UPDATE `${DB}`.api_keys SET revoked_at=NOW() | |
| 54 | WHERE id='$kid' AND user_id='$uid' | |
| 55 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 56 | $flash = 'Key revoked.'; | |
| 57 | } | |
| 58 | } | |
| 59 | ||
| 60 | my @keys = $db->db_readwrite_multiple($dbh, qq~ | |
| 61 | SELECT id, name, token_prefix, last_used_at, revoked_at, created_at | |
| 62 | FROM `${DB}`.api_keys | |
| 63 | WHERE user_id='$uid' | |
| 64 | ORDER BY id DESC | |
| 65 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 66 | ||
| 67 | $db->db_disconnect($dbh); | |
| 68 | ||
| 69 | my $body = qq~ | |
| 70 | <div class="page-header" style="display:flex;align-items:center;justify-content:space-between;margin-bottom:18px"> | |
| 71 | <div> | |
| 72 | <h1 style="margin:0;font-size:28px">API Keys</h1> | |
| 73 | <p class="text-secondary" style="margin:6px 0 0;font-size:14px">Programmatic access for scripts and integrations. Treat each token like a password.</p> | |
| 74 | </div> | |
| 75 | <form method="POST" style="display:flex;gap:8px"> | |
| 76 | <input type="hidden" name="op" value="create"> | |
| 77 | <input type="text" name="name" placeholder="Key name (e.g. 'Zapier production')" required style="padding:8px 12px;border:1px solid var(--col-border);border-radius:8px;background:var(--col-surface-2);color:var(--col-text);font-size:14px;width:240px"> | |
| 78 | <button type="submit" class="btn btn-primary" style="white-space:nowrap">+ New key</button> | |
| 79 | </form> | |
| 80 | </div> | |
| 81 | ~; | |
| 82 | $body .= qq~<div class="card" style="padding:16px;margin-bottom:18px;background:rgba(6,182,212,0.10);border:1px solid rgba(6,182,212,0.35);border-radius:10px;color:#67e8f9">$flash</div>~ if $flash; | |
| 83 | ||
| 84 | if (!@keys) { | |
| 85 | $body .= qq~<div class="card" style="padding:32px;text-align:center;color:var(--col-text-2)">No keys yet. Create one above to start using the REST API. See <a href="/api_docs.cgi" style="color:#67e8f9">API docs</a> for endpoints.</div>~; | |
| 86 | } else { | |
| 87 | $body .= q~<div class="card" style="padding:0;overflow:hidden"><table class="table" style="width:100%"><thead><tr><th style="text-align:left;padding:12px 16px">Name</th><th style="text-align:left;padding:12px 16px">Prefix</th><th style="text-align:left;padding:12px 16px">Last used</th><th style="text-align:left;padding:12px 16px">Created</th><th style="text-align:right;padding:12px 16px">Action</th></tr></thead><tbody>~; | |
| 88 | foreach my $k (@keys) { | |
| 89 | my $status = $k->{revoked_at} ? '<span style="color:#ef4444">revoked</span>' : '<span style="color:#10b981">active</span>'; | |
| 90 | my $last = $k->{last_used_at} || 'never'; | |
| 91 | my $action = $k->{revoked_at} ? '—' : qq~<form method="POST" style="display:inline" onsubmit="return confirm('Revoke this key? Any script using it will stop working.')"><input type="hidden" name="op" value="revoke"><input type="hidden" name="id" value="$k->{id}"><button type="submit" class="btn btn-secondary btn-sm" style="background:transparent;border:1px solid #ef4444;color:#ef4444">Revoke</button></form>~; | |
| 92 | $body .= qq~<tr><td style="padding:12px 16px">$k->{name} · $status</td><td style="padding:12px 16px;font-family:monospace;font-size:13px">$k->{token_prefix}…</td><td style="padding:12px 16px">$last</td><td style="padding:12px 16px">$k->{created_at}</td><td style="padding:12px 16px;text-align:right">$action</td></tr>~; | |
| 93 | } | |
| 94 | $body .= '</tbody></table></div>'; | |
| 95 | } | |
| 96 | ||
| 97 | print qq~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~; | |
| 98 | $wrap->render({userinfo=>$userinfo, page_key=>'api_keys', title=>'API Keys', body=>$body}); |