Diff -- /var/www/vhosts/3dshawn.com/admin.3dshawn.com/settings.cgi
Diff
/var/www/vhosts/3dshawn.com/admin.3dshawn.com/settings.cgi
added on local at 2026-07-01 14:22:48
Added
+0
lines
Removed
-0
lines
Context
191
unchanged
Blobs
from 47919bf595e4
to 47919bf595e4
to 47919bf595e4
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.| 1 | 1 | #!/usr/bin/perl |
| 2 | 2 | #====================================================================== |
| 3 | 3 | # Meta-Admin -- /settings.cgi |
| 4 | 4 | # |
| 5 | 5 | # Editable platform_settings for the meta-admin app itself: brand, |
| 6 | 6 | # cookie, session minutes, and the AES encryption key used to scramble |
| 7 | 7 | # site_connections passwords at rest. Renders an editable card per |
| 8 | 8 | # setting; POSTs to /settings_action.cgi. |
| 9 | 9 | #====================================================================== |
| 10 | 10 | use strict; |
| 11 | 11 | use warnings; |
| 12 | 12 | use lib '/var/www/vhosts/3dshawn.com/admin.3dshawn.com'; |
| 13 | 13 | use CGI; |
| 14 | 14 | use MODS::Login; |
| 15 | 15 | use MODS::DBConnect; |
| 16 | 16 | use MODS::MetaAdmin::Config; |
| 17 | 17 | use MODS::MetaAdmin::Wrapper; |
| 18 | 18 | |
| 19 | 19 | my $q = CGI->new; |
| 20 | 20 | my $form = $q->Vars; |
| 21 | 21 | my $auth = MODS::Login->new; |
| 22 | 22 | my $cfg = MODS::MetaAdmin::Config->new; |
| 23 | 23 | my $wrap = MODS::MetaAdmin::Wrapper->new; |
| 24 | 24 | my $u = $auth->login_verify; |
| 25 | 25 | |
| 26 | 26 | my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'settings'; |
| 27 | 27 | |
| 28 | 28 | if ($entry eq 'settings_action') { |
| 29 | 29 | unless ($u) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; } |
| 30 | 30 | _handle_action($q, $form); |
| 31 | 31 | exit; |
| 32 | 32 | } |
| 33 | 33 | |
| 34 | 34 | unless ($u) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; } |
| 35 | 35 | |
| 36 | 36 | # Pull every platform_settings row so the editor surfaces ALL keys, |
| 37 | 37 | # not just the ones the code reads. Adding a new row to the DB makes |
| 38 | 38 | # it editable here automatically. |
| 39 | 39 | my $db = MODS::DBConnect->new; |
| 40 | 40 | my $dbh = $db->db_connect; |
| 41 | 41 | my @rows = $db->db_readwrite_multiple($dbh, q~ |
| 42 | 42 | SELECT setting_key, setting_value, is_secret, updated_at |
| 43 | 43 | FROM platform_settings ORDER BY setting_key |
| 44 | 44 | ~, __FILE__, __LINE__); |
| 45 | 45 | $db->db_disconnect($dbh); |
| 46 | 46 | |
| 47 | 47 | # Metadata for the keys we know about -- label + group + description. |
| 48 | 48 | # Anything not in this list still renders under "Other" so the page |
| 49 | 49 | # never hides a settable value. |
| 50 | 50 | my %META = ( |
| 51 | 51 | brand_name => { label => 'Brand name', group => 'Brand', secret => 0, |
| 52 | 52 | desc => 'Shows in the sidebar header + browser title.' }, |
| 53 | 53 | brand_tagline => { label => 'Brand tagline', group => 'Brand', secret => 0, |
| 54 | 54 | desc => 'One-line tag under the brand name.' }, |
| 55 | 55 | cookie_name => { label => 'Session cookie name', group => 'Session', secret => 0, |
| 56 | 56 | desc => 'Name of the cookie used to keep you signed in. Changing kicks every open session.' }, |
| 57 | 57 | session_minutes => { label => 'Session minutes', group => 'Session', secret => 0, |
| 58 | 58 | desc => 'How long a session lasts before re-login. Default 720 (12h).' }, |
| 59 | 59 | site_conn_enc_key => { label => 'Site-connection encryption key', group => 'Security', secret => 1, |
| 60 | 60 | desc => 'AES key used to encrypt site_connections passwords at rest. Rotating invalidates every saved site password (must re-enter on Sites).' }, |
| 61 | 61 | ); |
| 62 | 62 | my @group_order = qw(Brand Session Security Other); |
| 63 | 63 | |
| 64 | 64 | # Bucket rows by group. |
| 65 | 65 | my %by_group; |
| 66 | 66 | foreach my $r (@rows) { |
| 67 | 67 | my $key = $r->{setting_key}; |
| 68 | 68 | my $m = $META{$key} || { label => $key, group => 'Other', secret => $r->{is_secret} ? 1 : 0, desc => '' }; |
| 69 | 69 | push @{ $by_group{ $m->{group} } }, { |
| 70 | 70 | key => $key, |
| 71 | 71 | value => $r->{setting_value}, |
| 72 | 72 | secret => $m->{secret} || $r->{is_secret} ? 1 : 0, |
| 73 | 73 | label => $m->{label}, |
| 74 | 74 | desc => $m->{desc} || '', |
| 75 | 75 | updated_at => $r->{updated_at} || '', |
| 76 | 76 | }; |
| 77 | 77 | } |
| 78 | 78 | |
| 79 | 79 | sub _esc { |
| 80 | 80 | my $s = shift; $s //= ''; |
| 81 | 81 | $s =~ s/&/&/g; $s =~ s/</</g; $s =~ s/>/>/g; |
| 82 | 82 | $s =~ s/"/"/g; |
| 83 | 83 | return $s; |
| 84 | 84 | } |
| 85 | 85 | |
| 86 | 86 | my $body = ''; |
| 87 | 87 | |
| 88 | 88 | # Flash banners |
| 89 | 89 | my $flash = ''; |
| 90 | 90 | if ($form->{saved}) { $flash = qq~<div class="banner success">Settings saved.</div>~; } |
| 91 | 91 | if ($form->{err}) { |
| 92 | 92 | my $e = _esc($form->{err}); |
| 93 | 93 | $flash = qq~<div class="banner danger">$e</div>~; |
| 94 | 94 | } |
| 95 | 95 | |
| 96 | 96 | $body .= q~ |
| 97 | 97 | <div class="page-head"><div class="left"> |
| 98 | 98 | <span class="page-eyebrow"><span class="dot"></span> Operator</span> |
| 99 | 99 | <h1 class="page-title">Settings</h1> |
| 100 | 100 | <p class="page-subtitle">Brand, session, and security keys for the meta-admin app itself. Site DB credentials live under <a href="/sites.cgi">Sites</a>.</p> |
| 101 | 101 | </div></div> |
| 102 | 102 | ~; |
| 103 | 103 | $body .= $flash; |
| 104 | 104 | |
| 105 | 105 | # One module per group with one form per row. |
| 106 | 106 | foreach my $g (@group_order) { |
| 107 | 107 | my $list = $by_group{$g} or next; |
| 108 | 108 | next unless scalar @$list; |
| 109 | 109 | my $glow = $g eq 'Security' ? 'glow-magenta' |
| 110 | 110 | : $g eq 'Brand' ? 'glow-cyan' |
| 111 | 111 | : $g eq 'Session' ? 'glow-violet' |
| 112 | 112 | : 'glow-lime'; |
| 113 | 113 | my $g_h = _esc($g); |
| 114 | 114 | $body .= qq~<div class="module $glow" style="margin-bottom:16px"> |
| 115 | 115 | <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></div><div class="module-title">$g_h</div></div></div> |
| 116 | 116 | <div class="module-body">~; |
| 117 | 117 | foreach my $r (@$list) { |
| 118 | 118 | my $k = _esc($r->{key}); |
| 119 | 119 | my $lbl = _esc($r->{label}); |
| 120 | 120 | my $desc = _esc($r->{desc}); |
| 121 | 121 | my $val = _esc($r->{value} // ''); |
| 122 | 122 | my $upd = _esc($r->{updated_at}); |
| 123 | 123 | my $input; |
| 124 | 124 | if ($r->{secret}) { |
| 125 | 125 | $input = qq~<input type="password" name="value" placeholder="(unchanged -- leave blank to keep)" autocomplete="new-password">~; |
| 126 | 126 | } elsif (length($r->{value} // '') > 80) { |
| 127 | 127 | $input = qq~<textarea name="value" rows="3">$val</textarea>~; |
| 128 | 128 | } else { |
| 129 | 129 | $input = qq~<input type="text" name="value" value="$val">~; |
| 130 | 130 | } |
| 131 | 131 | $body .= qq~<form method="POST" action="/settings_action.cgi" style="border-bottom:1px solid var(--bg-line);padding:12px 0;display:grid;grid-template-columns:240px 1fr auto;gap:14px;align-items:start"> |
| 132 | 132 | <input type="hidden" name="op" value="save"> |
| 133 | 133 | <input type="hidden" name="key" value="$k"> |
| 134 | 134 | <div> |
| 135 | 135 | <div style="font-weight:600;font-size:13px;color:var(--col-text)">$lbl</div> |
| 136 | 136 | <div style="font-size:11px;color:var(--col-text-3);margin-top:4px;font-family:'JetBrains Mono',monospace">$k</div> |
| 137 | 137 | <div style="font-size:11px;color:var(--col-text-dim);margin-top:6px;line-height:1.5">$desc</div> |
| 138 | 138 | <div style="font-size:10px;color:var(--col-text-dim);margin-top:6px">Updated $upd</div> |
| 139 | 139 | </div> |
| 140 | 140 | <div>$input</div> |
| 141 | 141 | <button type="submit" class="btn btn-sm">Save</button> |
| 142 | 142 | </form>~; |
| 143 | 143 | } |
| 144 | 144 | $body .= q~</div></div>~; |
| 145 | 145 | } |
| 146 | 146 | |
| 147 | 147 | $wrap->render(title => 'Settings', page_key => 'settings', body => $body, userinfo => $u); |
| 148 | 148 | |
| 149 | 149 | #====================================================================== |
| 150 | 150 | # settings_action entry: POST handler |
| 151 | 151 | # op=save -- UPSERT a single platform_settings row by key. |
| 152 | 152 | #====================================================================== |
| 153 | 153 | sub _act_go { |
| 154 | 154 | my $q = shift || ''; |
| 155 | 155 | print "Status: 302 Found\nLocation: /settings.cgi$q\n\n"; |
| 156 | 156 | } |
| 157 | 157 | sub _act_url_esc { |
| 158 | 158 | my $s = shift; $s //= ''; |
| 159 | 159 | $s =~ s/([^A-Za-z0-9 .,'\$+-])/ /g; |
| 160 | 160 | $s =~ s/ /+/g; |
| 161 | 161 | return $s; |
| 162 | 162 | } |
| 163 | 163 | |
| 164 | 164 | sub _handle_action { |
| 165 | 165 | my ($q, $f) = @_; |
| 166 | 166 | |
| 167 | 167 | my $op = lc($f->{op} || ''); |
| 168 | 168 | my $key = $f->{key} // ''; |
| 169 | 169 | my $val = $f->{value}; # blank means "keep existing" |
| 170 | 170 | unless ($op eq 'save' && length $key) { _act_go('?err=' . _act_url_esc('bad input')); return; } |
| 171 | 171 | # Empty value on save means "keep existing" -- the secret pattern. |
| 172 | 172 | if (!defined $val || $val eq '') { _act_go('?saved=1'); return; } |
| 173 | 173 | |
| 174 | 174 | # Sanity-cap absurd values. |
| 175 | 175 | $val = substr($val, 0, 16384); |
| 176 | 176 | |
| 177 | 177 | my $db = MODS::DBConnect->new; |
| 178 | 178 | my $dbh = $db->db_connect; |
| 179 | 179 | unless ($dbh) { _act_go('?err=' . _act_url_esc('db unavailable')); return; } |
| 180 | 180 | my $k_q = $key; $k_q =~ s/'/''/g; |
| 181 | 181 | my $v_q = $val; $v_q =~ s/'/''/g; |
| 182 | 182 | $db->db_readwrite($dbh, qq~ |
| 183 | 183 | INSERT INTO platform_settings (setting_key, setting_value) |
| 184 | 184 | VALUES ('$k_q', '$v_q') |
| 185 | 185 | ON DUPLICATE KEY UPDATE setting_value=VALUES(setting_value), updated_at=NOW() |
| 186 | 186 | ~, __FILE__, __LINE__); |
| 187 | 187 | $db->db_disconnect($dbh); |
| 188 | 188 | |
| 189 | 189 | _act_go('?saved=1'); |
| 190 | 190 | return; |
| 191 | 191 | } |