Diff -- /var/www/vhosts/3dshawn.com/abforge.3dshawn.com/admin_maintenance.cgi

O Operator
Diff

/var/www/vhosts/3dshawn.com/abforge.3dshawn.com/admin_maintenance.cgi

added on local at 2026-07-01 16:00:54

Added
+214
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 66b5c86e989c
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# ABForge -- /admin_maintenance.cgi
4#
5# Lock the site for everyone except admins. Edit the
6# message that users see while the site is locked, with a live
7# split-view preview. Companion to admin_maintenance_action.cgi.
8#======================================================================
9use strict;
10use warnings;
11use lib '/var/www/vhosts/3dshawn.com/abforge.3dshawn.com';
12use CGI;
13use MODS::Login;
14use MODS::DBConnect;
15use MODS::ABForge::Config;
16use MODS::ABForge::Wrapper;
17
18my $q = CGI->new;
19my $form = $q->Vars;
20my $auth = MODS::Login->new;
21my $u = $auth->login_verify;
22
23my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'admin_maintenance';
24
25if ($entry eq 'admin_maintenance_action') {
26 unless ($u && $u->{is_admin}) {
27 print "Status: 403 Forbidden\nContent-Type: text/plain\n\nadmin only\n";
28 exit;
29 }
30 _handle_action($q, $form);
31 exit;
32}
33
34unless ($u) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; }
35unless ($u->{is_admin}) {
36 print "Status: 403 Forbidden\nContent-Type: text/html\n\n<h1>Forbidden</h1><p>Only admins can manage maintenance mode.</p>\n";
37 exit;
38}
39
40my $cfg = MODS::ABForge::Config->new;
41my $locked = int($cfg->settings('maintenance_locked') || 0);
42my $msg = $cfg->settings('maintenance_message') || '';
43
44sub _esc {
45 my $s = shift; $s //= '';
46 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g; $s =~ s/"/&quot;/g;
47 return $s;
48}
49
50my $flash = '';
51if ($form->{saved}) { $flash = '<div class="banner success">Maintenance message saved.</div>'; }
52if ($form->{locked}) { $flash = '<div class="banner warn"><strong>System is now LOCKED.</strong> Only admins can sign in or stay signed in.</div>'; }
53if ($form->{unlocked}) { $flash = '<div class="banner success">System is now open to all users.</div>'; }
54if ($form->{err}) { my $e = _esc($form->{err}); $flash = qq~<div class="banner danger">$e</div>~; }
55
56my $msg_h = _esc($msg);
57my $status_lbl = $locked ? 'LOCKED' : 'OPEN';
58my $status_pill_class = $locked ? 'pill-bad' : 'pill-ok';
59my $toggle_op = $locked ? 'unlock' : 'lock';
60my $toggle_lbl = $locked ? 'Unlock System' : 'Lock System';
61my $toggle_btn_class = $locked ? 'btn btn-primary' : 'btn btn-danger';
62my $toggle_confirm = $locked
63 ? "return confirm('Unlock the site? Regular users will be able to sign in again.');"
64 : "return confirm('Lock the site? Only admins will be able to sign in until you unlock. Existing non-admin sessions are killed immediately.');";
65
66my $body = qq~
67<div class="page-head"><div class="left">
68 <span class="page-eyebrow"><span class="dot"></span> Operations</span>
69 <h1 class="page-title">Maintenance mode</h1>
70 <p class="page-subtitle">Block all non-admin users from signing in (or staying signed in), with a custom message shown on the login page.</p>
71</div></div>
72$flash
73
74<div class="module" style="margin-bottom:16px">
75 <div class="module-head"><div class="left"><div class="module-title">Status</div></div></div>
76 <div class="module-body" style="display:flex;gap:16px;align-items:center;flex-wrap:wrap">
77 <span class="pill $status_pill_class" style="font-size:14px;padding:4px 12px">$status_lbl</span>
78 <form method="POST" action="/admin_maintenance_action.cgi" style="margin:0" onsubmit="$toggle_confirm">
79 <input type="hidden" name="op" value="$toggle_op">
80 <button type="submit" class="$toggle_btn_class">$toggle_lbl</button>
81 </form>
82 <div style="flex:1"></div>
83 <a href="/login.cgi?_preview_maintenance=1" target="_blank" class="btn">View as user (opens login)</a>
84 </div>
85</div>
86
87<div class="module">
88 <div class="module-head"><div class="left"><div class="module-title">Message users see while locked</div></div></div>
89 <div class="module-body">
90 <form method="POST" action="/admin_maintenance_action.cgi">
91 <input type="hidden" name="op" value="save_message">
92 <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:12px">
93 <div>
94 <label class="lbl"><span class="t">HTML source</span>
95 <style>
96 #maint_src{width:100%;height:360px;padding:12px 14px;background-color:#0a0a0a;color:#cbd5e1;color-scheme:dark;border:1px solid rgba(255,255,255,.08);border-radius:8px;font-family:'JetBrains Mono','Consolas',monospace;font-size:12px;line-height:1.5;resize:vertical;box-sizing:border-box}
97 </style>
98 <textarea id="maint_src" name="message" spellcheck="false">$msg_h</textarea>
99 </label>
100 <div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:4px;font-size:11px">
101 <button type="button" class="btn btn-sm" onclick="wrap('<strong>','</strong>')">B</button>
102 <button type="button" class="btn btn-sm" onclick="wrap('<em>','</em>')"><em>I</em></button>
103 <button type="button" class="btn btn-sm" onclick="wrap('<h2>','</h2>')">H2</button>
104 <button type="button" class="btn btn-sm" onclick="wrap('<p>','</p>')">P</button>
105 <button type="button" class="btn btn-sm" onclick="ins('<br>')">BR</button>
106 <button type="button" class="btn btn-sm" onclick="linkPrompt()">Link</button>
107 <button type="button" class="btn btn-sm" onclick="resetMsg()">Reset to default</button>
108 </div>
109 </div>
110 <div>
111 <label class="lbl"><span class="t">Live preview</span>
112 <iframe id="maint_preview"
113 style="width:100%;height:360px;background:#0a0a0a;border:1px solid var(--col-border);border-radius:5px"></iframe>
114 </label>
115 <div class="dim" style="font-size:11px;margin-top:4px">Preview updates as you type. Bg is the same dark theme users see on /login.cgi.</div>
116 </div>
117 </div>
118 <button type="submit" class="btn btn-primary">Save message</button>
119 </form>
120 </div>
121</div>
122
123<script>
124(function(){
125 var ta = document.getElementById('maint_src');
126 var pv = document.getElementById('maint_preview');
127 function refresh() {
128 var html = '<style>body{font-family:system-ui,-apple-system,Segoe UI,Arial,sans-serif;background:#0a0a0a;color:#e5e5e5;margin:0;padding:24px;min-height:100vh;display:flex;align-items:center;justify-content:center}</style>' + ta.value;
129 pv.srcdoc = html;
130 }
131 ta.addEventListener('input', refresh);
132 refresh();
133 window.wrap = function(o,c){
134 var s=ta.selectionStart, e=ta.selectionEnd, v=ta.value;
135 ta.value = v.slice(0,s) + o + v.slice(s,e) + c + v.slice(e);
136 ta.focus(); ta.selectionStart=s+o.length; ta.selectionEnd=e+o.length; refresh();
137 };
138 window.ins = function(t){
139 var s=ta.selectionStart, v=ta.value;
140 ta.value = v.slice(0,s) + t + v.slice(s);
141 ta.focus(); ta.selectionStart=ta.selectionEnd=s+t.length; refresh();
142 };
143 window.linkPrompt = function(){
144 var url = prompt('Link URL', 'https://');
145 if (!url) return;
146 wrap('<a href="' + url + '">', '</a>');
147 };
148 window.resetMsg = function(){
149 if (!confirm('Reset to default message? Unsaved edits will be lost.')) return;
150 ta.value = '<div style="text-align:center;padding:40px 20px;max-width:560px;margin:0 auto">' +
151 '<h2 style="margin:0 0 14px;font-size:28px">We will be right back</h2>' +
152 '<p style="margin:0 0 14px;color:#aaa;font-size:16px;line-height:1.6">We are rolling out a quick improvement to the system. Most updates wrap up in under an hour.</p>' +
153 '<p style="margin:24px 0 0;color:#888;font-size:13px">Thanks for your patience. -- The team</p>' +
154 '</div>';
155 refresh();
156 };
157})();
158</script>
159~;
160
161print "Content-Type: text/html; charset=utf-8
162Cache-Control: no-cache
163
164";
165MODS::ABForge::Wrapper->new->render({ userinfo => $u, title => 'Maintenance', page_key => 'admin_maintenance', body => $body });
166
167#======================================================================
168# admin_maintenance_action entry: POST handler
169# op=lock -- set maintenance_locked = 1
170# op=unlock -- set maintenance_locked = 0
171# op=save_message -- save the HTML body shown to locked users
172#======================================================================
173sub _handle_action {
174 my ($q, $form) = @_;
175
176 my $db = MODS::DBConnect->new;
177 my $dbh = $db->db_connect;
178 unless ($dbh) {
179 print "Status: 302 Found\nLocation: /admin_maintenance.cgi?err=db_unavailable\n\n";
180 return;
181 }
182
183 my $op = $form->{op} || '';
184
185 if ($op eq 'lock' || $op eq 'unlock') {
186 my $v = ($op eq 'lock') ? '1' : '0';
187 $db->db_readwrite($dbh, qq~
188 INSERT INTO platform_settings (setting_key, setting_value)
189 VALUES ('maintenance_locked', '$v')
190 ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)
191 ~, __FILE__, __LINE__);
192 $db->db_disconnect($dbh);
193 my $flag = ($op eq 'lock') ? 'locked=1' : 'unlocked=1';
194 print "Status: 302 Found\nLocation: /admin_maintenance.cgi?$flag\n\n";
195 return;
196 }
197 elsif ($op eq 'save_message') {
198 my $msg = $form->{message} // '';
199 # Clamp size to keep platform_settings reasonable
200 $msg = substr($msg, 0, 20000) if length($msg) > 20000;
201 my $msg_q = $msg; $msg_q =~ s/'/''/g; $msg_q =~ s/\\/\\\\/g;
202 $db->db_readwrite($dbh, qq~
203 INSERT INTO platform_settings (setting_key, setting_value)
204 VALUES ('maintenance_message', '$msg_q')
205 ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)
206 ~, __FILE__, __LINE__);
207 $db->db_disconnect($dbh);
208 print "Status: 302 Found\nLocation: /admin_maintenance.cgi?saved=1\n\n";
209 return;
210 }
211
212 $db->db_disconnect($dbh);
213 print "Status: 302 Found\nLocation: /admin_maintenance.cgi?err=unknown_op\n\n";
214}