Add admin login overlay and gated UI
Introduce a client-side admin login screen that locks the admin page until the correct password is entered. admelist.html now contains a dedicated admin login card and wraps the actual admin shell content in a hidden section; the body receives an admin-locked class while locked. app.js adds form handling, verifyAdminPassword/unlockAdminShell logic, uses a DELETE /__authcheck__ request to validate the stored password, and shows/hides the admin UI accordingly; it also centralizes auth status messages. styles.css adds styles for the login overlay, locked state, and responsive tweaks. server/server.js removes the previous server-side requireAdmin guard for /admelist so the client can perform the auth check.
This commit is contained in:
+25
-12
@@ -6,8 +6,8 @@
|
||||
<title>Edit List - GD fedl</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body data-page="admelist">
|
||||
<header>
|
||||
<body data-page="admelist" class="admin-locked">
|
||||
<header id="admin-page-header">
|
||||
<h1>FEDL Admin</h1>
|
||||
<nav>
|
||||
<a href="index.html">Home</a>
|
||||
@@ -19,22 +19,34 @@
|
||||
</nav>
|
||||
</header>
|
||||
<main class="admin-shell">
|
||||
<section id="admin-login-screen" class="admin-login-screen">
|
||||
<div class="admin-login-card">
|
||||
<p class="hero-kicker">Admin Access</p>
|
||||
<h2>Enter the admin password</h2>
|
||||
<p class="muted">Submit the server password to unlock the list editor and run moderation tools.</p>
|
||||
<form id="admin-login-form" class="admin-login-form">
|
||||
<label class="admin-password-field" for="admin-password">
|
||||
<span>Admin password</span>
|
||||
<input id="admin-password" type="password" placeholder="Enter server password" autocomplete="current-password" />
|
||||
</label>
|
||||
<div class="admin-login-actions">
|
||||
<button id="admin-login-submit" type="submit" class="btn">Submit</button>
|
||||
<p id="admin-auth-status" class="muted">Saved only in this browser session.</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="admin-shell-content" class="admin-shell-content" hidden>
|
||||
<section class="panel admin-overview-panel">
|
||||
<div>
|
||||
<p class="hero-kicker">Control Center</p>
|
||||
<h2>Manage the live list and incoming runs</h2>
|
||||
<p class="muted">Use the first panel to edit placements and the second panel to review new run submissions.</p>
|
||||
</div>
|
||||
<div class="admin-access-panel">
|
||||
<div class="admin-overview-badges">
|
||||
<span>Live list editor</span>
|
||||
<span>Run moderation</span>
|
||||
</div>
|
||||
<label class="admin-password-field" for="admin-password">
|
||||
<span>Admin password</span>
|
||||
<input id="admin-password" type="password" placeholder="Enter server password" autocomplete="current-password" />
|
||||
</label>
|
||||
<p id="admin-auth-status" class="muted">Saved only in this browser session.</p>
|
||||
<div class="admin-overview-badges">
|
||||
<span>Live list editor</span>
|
||||
<span>Run moderation</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -95,6 +107,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -452,6 +452,9 @@
|
||||
}
|
||||
|
||||
if(page==='admelist'){
|
||||
const loginScreenEl = qs('admin-login-screen');
|
||||
const adminShellContentEl = qs('admin-shell-content');
|
||||
const loginFormEl = qs('admin-login-form');
|
||||
const statusEl = qs('admin-status');
|
||||
const listTbody = qs('admin-list-body');
|
||||
const addBtn = qs('add-row');
|
||||
@@ -481,7 +484,7 @@
|
||||
authStatusEl.textContent = password
|
||||
? 'Password saved for this browser session.'
|
||||
: 'Saved only in this browser session.';
|
||||
authStatusEl.classList.remove('error-text');
|
||||
if(password) authStatusEl.classList.remove('error-text');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,9 +499,54 @@
|
||||
|
||||
function handleAdminAuthFailure(message, targetSetter){
|
||||
setAdminPassword('');
|
||||
document.body.classList.add('admin-locked');
|
||||
if(adminShellContentEl) adminShellContentEl.hidden = true;
|
||||
if(loginScreenEl) loginScreenEl.hidden = false;
|
||||
targetSetter(message || 'Admin password required or incorrect.', true);
|
||||
}
|
||||
|
||||
function unlockAdminShell(){
|
||||
document.body.classList.remove('admin-locked');
|
||||
if(loginScreenEl) loginScreenEl.hidden = true;
|
||||
if(adminShellContentEl) adminShellContentEl.hidden = false;
|
||||
}
|
||||
|
||||
function verifyAdminPassword(){
|
||||
if(!getAdminPassword()){
|
||||
handleAdminAuthFailure('Enter the admin password to continue.', function(message, isError){
|
||||
if(!authStatusEl) return;
|
||||
authStatusEl.textContent = message;
|
||||
authStatusEl.classList.toggle('error-text', !!isError);
|
||||
});
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return fetch(`${liveRunsUrl}/__authcheck__`, {
|
||||
method:'DELETE',
|
||||
headers:authHeaders()
|
||||
}).then(r=>{
|
||||
if(r.status === 401) throw new Error('Admin auth failed');
|
||||
if(r.status !== 404) throw new Error('Admin verify failed');
|
||||
return true;
|
||||
}).then(ok=>{
|
||||
unlockAdminShell();
|
||||
if(authStatusEl){
|
||||
authStatusEl.textContent = 'Access granted for this browser session.';
|
||||
authStatusEl.classList.remove('error-text');
|
||||
}
|
||||
loadAdmin();
|
||||
loadRunsAdmin();
|
||||
return ok;
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
handleAdminAuthFailure('Wrong admin password. Try again.', function(message, isError){
|
||||
if(!authStatusEl) return;
|
||||
authStatusEl.textContent = message;
|
||||
authStatusEl.classList.toggle('error-text', !!isError);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function setStatus(message, isError){
|
||||
if(!statusEl) return;
|
||||
statusEl.textContent = message;
|
||||
@@ -839,8 +887,16 @@
|
||||
}
|
||||
if(adminPasswordEl){
|
||||
setAdminPassword(getAdminPassword());
|
||||
adminPasswordEl.addEventListener('input', function(){
|
||||
setAdminPassword(adminPasswordEl.value.trim());
|
||||
}
|
||||
if(loginFormEl){
|
||||
loginFormEl.addEventListener('submit', function(event){
|
||||
event.preventDefault();
|
||||
setAdminPassword((adminPasswordEl && adminPasswordEl.value || '').trim());
|
||||
if(authStatusEl){
|
||||
authStatusEl.textContent = 'Checking password...';
|
||||
authStatusEl.classList.remove('error-text');
|
||||
}
|
||||
verifyAdminPassword();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -863,8 +919,15 @@
|
||||
setRunsStatus('Run queue reloaded from the live server.');
|
||||
});
|
||||
|
||||
loadAdmin();
|
||||
loadRunsAdmin();
|
||||
if(getAdminPassword()){
|
||||
verifyAdminPassword();
|
||||
}else{
|
||||
handleAdminAuthFailure('Enter the admin password to continue.', function(message, isError){
|
||||
if(!authStatusEl) return;
|
||||
authStatusEl.textContent = message;
|
||||
authStatusEl.classList.toggle('error-text', !!isError);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(page==='run'){
|
||||
|
||||
@@ -297,10 +297,6 @@ const server = http.createServer((req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((pathname === '/admelist.html' || pathname === '/admelist') && !requireAdmin(req, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
serveFile(pathname, res);
|
||||
});
|
||||
|
||||
|
||||
+12
-2
@@ -34,10 +34,20 @@ table.levels-table tr:hover td{background:rgba(255,255,255,0.01)}
|
||||
.small-btn{padding:8px 12px}
|
||||
.danger-btn{background:#ff7b7b;color:#2a0909}
|
||||
.admin-shell{padding:18px 20px}
|
||||
.admin-shell-content[hidden]{display:none}
|
||||
.admin-locked{overflow:hidden}
|
||||
.admin-locked #admin-page-header,.admin-locked #admin-shell-content{visibility:hidden}
|
||||
.admin-login-screen{position:fixed;inset:0;z-index:2000;display:flex;align-items:center;justify-content:center;padding:30px;background:
|
||||
radial-gradient(circle at top,rgba(255,184,77,0.16),transparent 24%),
|
||||
linear-gradient(180deg,rgba(6,16,33,0.98),rgba(7,20,38,0.99))}
|
||||
.admin-login-card{width:min(100%,520px);padding:34px;border-radius:28px;background:linear-gradient(145deg,rgba(255,184,77,0.12),rgba(7,19,38,0.96) 42%),radial-gradient(circle at top left,rgba(255,255,255,0.08),transparent 35%);border:1px solid rgba(255,184,77,0.16)}
|
||||
.admin-login-card h2{margin:0 0 10px 0;font-size:clamp(2rem,4vw,2.8rem)}
|
||||
.admin-login-form{display:grid;gap:16px;margin-top:20px}
|
||||
.admin-login-actions{display:grid;gap:12px}
|
||||
.admin-login-screen[hidden]{display:none}
|
||||
.admin-panel{padding:20px}
|
||||
.admin-overview-panel{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:24px 26px;border-radius:28px;background:linear-gradient(135deg,rgba(255,184,77,0.12),rgba(7,19,38,0.95));border:1px solid rgba(255,184,77,0.16)}
|
||||
.admin-overview-panel h2{margin:0 0 8px 0;font-size:clamp(1.9rem,4vw,2.8rem)}
|
||||
.admin-access-panel{display:grid;gap:12px;justify-items:start;min-width:min(100%,320px)}
|
||||
.admin-overview-badges{display:flex;flex-wrap:wrap;gap:10px}
|
||||
.admin-overview-badges span{padding:10px 14px;border-radius:999px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);color:#eef5ff;font-weight:700}
|
||||
.admin-password-field{display:grid;gap:8px;width:100%;font-weight:700;color:#f2f7fd}
|
||||
@@ -259,5 +269,5 @@ linear-gradient(180deg,rgba(8,18,32,0.98),rgba(7,19,38,0.94))}
|
||||
.status-card span{display:block;color:var(--muted);line-height:1.5}
|
||||
@media(max-width:900px){.layout{flex-direction:column}.sidebar{width:100%}.sidebar .panel{height:auto}.video-modal iframe{height:320px}.home-hero{grid-template-columns:1fr;min-height:auto}.home-copy,.hero-stage{padding:24px 22px}.hero-stats,.home-section-grid,.spotlight-grid{grid-template-columns:1fr}.spotlight-panel{grid-template-columns:1fr;padding:24px 22px}.list-hero-panel,.table-header,.search-row,.roulette-head{flex-direction:column;align-items:flex-start}.control-select{max-width:none;width:100%}.roulette-shell{grid-template-columns:1fr}.discord-panel{flex-direction:column;align-items:flex-start;padding:22px 20px}.discord-panel h2{font-size:1.55rem}.discord-actions{width:100%;justify-content:flex-start}.rules-view header{padding:16px 18px}.rules-view header h1{font-size:1.55rem}.rules-page{padding:24px 20px}.rules-page h2{font-size:1.65rem}}
|
||||
@media(max-width:900px){.error-grid{grid-template-columns:1fr}.error-copy,.error-side{padding:24px 20px}.error-copy h2{font-size:2.5rem}}
|
||||
@media(max-width:900px){.admin-overview-panel,.admin-toolbar{flex-direction:column;align-items:stretch}.admin-actions{flex-direction:column;align-items:stretch}.admin-password-field input,.admin-actions input[type=text],.admin-actions input[type=password]{min-width:0;width:100%}.run-shell{grid-template-columns:1fr}.run-head{flex-direction:column;align-items:flex-start}.run-form{grid-template-columns:1fr}}
|
||||
@media(max-width:900px){.admin-overview-panel,.admin-toolbar{flex-direction:column;align-items:stretch}.admin-actions{flex-direction:column;align-items:stretch}.admin-password-field input,.admin-actions input[type=text],.admin-actions input[type=password]{min-width:0;width:100%}.run-shell{grid-template-columns:1fr}.run-head{flex-direction:column;align-items:flex-start}.run-form{grid-template-columns:1fr}.admin-login-screen{padding:18px}.admin-login-card{padding:26px 22px}}
|
||||
@media(max-width:640px){header{flex-direction:column;align-items:flex-start;gap:12px}nav{display:flex;flex-wrap:wrap;gap:10px}nav a{margin-left:0}.home-copy h2{max-width:none}.hero-actions{flex-direction:column;align-items:stretch}.hero-actions .btn,.roulette-btn{width:100%;text-align:center}.list-hero-panel,.list-controls-panel,.list-table-panel,.roulette-panel,.roulette-rules-card,.run-panel,.run-side-panel,.submission-panel{padding:22px 18px}.table-hint{text-align:left}.run-admin-actions{justify-content:flex-start}}
|
||||
|
||||
Reference in New Issue
Block a user