Diff -- /var/www/vhosts/3dshawn.com/admin.3dshawn.com/settings.cgi

O Operator
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
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
11#!/usr/bin/perl
22#======================================================================
33# Meta-Admin -- /settings.cgi
44#
55# Editable platform_settings for the meta-admin app itself: brand,
66# cookie, session minutes, and the AES encryption key used to scramble
77# site_connections passwords at rest. Renders an editable card per
88# setting; POSTs to /settings_action.cgi.
99#======================================================================
1010use strict;
1111use warnings;
1212use lib '/var/www/vhosts/3dshawn.com/admin.3dshawn.com';
1313use CGI;
1414use MODS::Login;
1515use MODS::DBConnect;
1616use MODS::MetaAdmin::Config;
1717use MODS::MetaAdmin::Wrapper;
1818
1919my $q = CGI->new;
2020my $form = $q->Vars;
2121my $auth = MODS::Login->new;
2222my $cfg = MODS::MetaAdmin::Config->new;
2323my $wrap = MODS::MetaAdmin::Wrapper->new;
2424my $u = $auth->login_verify;
2525
2626my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'settings';
2727
2828if ($entry eq 'settings_action') {
2929 unless ($u) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; }
3030 _handle_action($q, $form);
3131 exit;
3232}
3333
3434unless ($u) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; }
3535
3636# Pull every platform_settings row so the editor surfaces ALL keys,
3737# not just the ones the code reads. Adding a new row to the DB makes
3838# it editable here automatically.
3939my $db = MODS::DBConnect->new;
4040my $dbh = $db->db_connect;
4141my @rows = $db->db_readwrite_multiple($dbh, q~
4242 SELECT setting_key, setting_value, is_secret, updated_at
4343 FROM platform_settings ORDER BY setting_key
4444~, __FILE__, __LINE__);
4545$db->db_disconnect($dbh);
4646
4747# Metadata for the keys we know about -- label + group + description.
4848# Anything not in this list still renders under "Other" so the page
4949# never hides a settable value.
5050my %META = (
5151 brand_name => { label => 'Brand name', group => 'Brand', secret => 0,
5252 desc => 'Shows in the sidebar header + browser title.' },
5353 brand_tagline => { label => 'Brand tagline', group => 'Brand', secret => 0,
5454 desc => 'One-line tag under the brand name.' },
5555 cookie_name => { label => 'Session cookie name', group => 'Session', secret => 0,
5656 desc => 'Name of the cookie used to keep you signed in. Changing kicks every open session.' },
5757 session_minutes => { label => 'Session minutes', group => 'Session', secret => 0,
5858 desc => 'How long a session lasts before re-login. Default 720 (12h).' },
5959 site_conn_enc_key => { label => 'Site-connection encryption key', group => 'Security', secret => 1,
6060 desc => 'AES key used to encrypt site_connections passwords at rest. Rotating invalidates every saved site password (must re-enter on Sites).' },
6161);
6262my @group_order = qw(Brand Session Security Other);
6363
6464# Bucket rows by group.
6565my %by_group;
6666foreach my $r (@rows) {
6767 my $key = $r->{setting_key};
6868 my $m = $META{$key} || { label => $key, group => 'Other', secret => $r->{is_secret} ? 1 : 0, desc => '' };
6969 push @{ $by_group{ $m->{group} } }, {
7070 key => $key,
7171 value => $r->{setting_value},
7272 secret => $m->{secret} || $r->{is_secret} ? 1 : 0,
7373 label => $m->{label},
7474 desc => $m->{desc} || '',
7575 updated_at => $r->{updated_at} || '',
7676 };
7777}
7878
7979sub _esc {
8080 my $s = shift; $s //= '';
8181 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g;
8282 $s =~ s/"/&quot;/g;
8383 return $s;
8484}
8585
8686my $body = '';
8787
8888# Flash banners
8989my $flash = '';
9090if ($form->{saved}) { $flash = qq~<div class="banner success">Settings saved.</div>~; }
9191if ($form->{err}) {
9292 my $e = _esc($form->{err});
9393 $flash = qq~<div class="banner danger">$e</div>~;
9494}
9595
9696$body .= q~
9797<div class="page-head"><div class="left">
9898 <span class="page-eyebrow"><span class="dot"></span> Operator</span>
9999 <h1 class="page-title">Settings</h1>
100100 <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>
101101</div></div>
102102~;
103103$body .= $flash;
104104
105105# One module per group with one form per row.
106106foreach my $g (@group_order) {
107107 my $list = $by_group{$g} or next;
108108 next unless scalar @$list;
109109 my $glow = $g eq 'Security' ? 'glow-magenta'
110110 : $g eq 'Brand' ? 'glow-cyan'
111111 : $g eq 'Session' ? 'glow-violet'
112112 : 'glow-lime';
113113 my $g_h = _esc($g);
114114 $body .= qq~<div class="module $glow" style="margin-bottom:16px">
115115 <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>
116116 <div class="module-body">~;
117117 foreach my $r (@$list) {
118118 my $k = _esc($r->{key});
119119 my $lbl = _esc($r->{label});
120120 my $desc = _esc($r->{desc});
121121 my $val = _esc($r->{value} // '');
122122 my $upd = _esc($r->{updated_at});
123123 my $input;
124124 if ($r->{secret}) {
125125 $input = qq~<input type="password" name="value" placeholder="(unchanged -- leave blank to keep)" autocomplete="new-password">~;
126126 } elsif (length($r->{value} // '') > 80) {
127127 $input = qq~<textarea name="value" rows="3">$val</textarea>~;
128128 } else {
129129 $input = qq~<input type="text" name="value" value="$val">~;
130130 }
131131 $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">
132132 <input type="hidden" name="op" value="save">
133133 <input type="hidden" name="key" value="$k">
134134 <div>
135135 <div style="font-weight:600;font-size:13px;color:var(--col-text)">$lbl</div>
136136 <div style="font-size:11px;color:var(--col-text-3);margin-top:4px;font-family:'JetBrains Mono',monospace">$k</div>
137137 <div style="font-size:11px;color:var(--col-text-dim);margin-top:6px;line-height:1.5">$desc</div>
138138 <div style="font-size:10px;color:var(--col-text-dim);margin-top:6px">Updated $upd</div>
139139 </div>
140140 <div>$input</div>
141141 <button type="submit" class="btn btn-sm">Save</button>
142142 </form>~;
143143 }
144144 $body .= q~</div></div>~;
145145}
146146
147147$wrap->render(title => 'Settings', page_key => 'settings', body => $body, userinfo => $u);
148148
149149#======================================================================
150150# settings_action entry: POST handler
151151# op=save -- UPSERT a single platform_settings row by key.
152152#======================================================================
153153sub _act_go {
154154 my $q = shift || '';
155155 print "Status: 302 Found\nLocation: /settings.cgi$q\n\n";
156156}
157157sub _act_url_esc {
158158 my $s = shift; $s //= '';
159159 $s =~ s/([^A-Za-z0-9 .,'\$+-])/ /g;
160160 $s =~ s/ /+/g;
161161 return $s;
162162}
163163
164164sub _handle_action {
165165 my ($q, $f) = @_;
166166
167167 my $op = lc($f->{op} || '');
168168 my $key = $f->{key} // '';
169169 my $val = $f->{value}; # blank means "keep existing"
170170 unless ($op eq 'save' && length $key) { _act_go('?err=' . _act_url_esc('bad input')); return; }
171171 # Empty value on save means "keep existing" -- the secret pattern.
172172 if (!defined $val || $val eq '') { _act_go('?saved=1'); return; }
173173
174174 # Sanity-cap absurd values.
175175 $val = substr($val, 0, 16384);
176176
177177 my $db = MODS::DBConnect->new;
178178 my $dbh = $db->db_connect;
179179 unless ($dbh) { _act_go('?err=' . _act_url_esc('db unavailable')); return; }
180180 my $k_q = $key; $k_q =~ s/'/''/g;
181181 my $v_q = $val; $v_q =~ s/'/''/g;
182182 $db->db_readwrite($dbh, qq~
183183 INSERT INTO platform_settings (setting_key, setting_value)
184184 VALUES ('$k_q', '$v_q')
185185 ON DUPLICATE KEY UPDATE setting_value=VALUES(setting_value), updated_at=NOW()
186186 ~, __FILE__, __LINE__);
187187 $db->db_disconnect($dbh);
188188
189189 _act_go('?saved=1');
190190 return;
191191}