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

O Operator
Diff

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

added on local at 2026-07-01 22:09:18

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