Add admin password UI and auth handling
Add an admin password field and session-backed auth handling for admin endpoints. admelist.html: introduce an admin password input and status text in the admin overview panel. app.js: store the password in sessionStorage (key: fedl_admin_password), populate the input on load, and persist changes; provide authHeaders() to inject a Basic Authorization header (fedl:password) into fetch requests; handle 401 responses by clearing the saved password and showing contextual error messages; wire password usage into save/update/delete run APIs. styles.css: add styling for the admin access panel and password field and adjust responsive rules. This enables browser-session admin authentication and clearer failure handling when admin credentials are missing or incorrect.
This commit is contained in:
+10
-3
@@ -25,9 +25,16 @@
|
||||
<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-overview-badges">
|
||||
<span>Live list editor</span>
|
||||
<span>Run moderation</span>
|
||||
<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>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -457,11 +457,47 @@
|
||||
const addBtn = qs('add-row');
|
||||
const saveBtn = qs('save-list');
|
||||
const searchEl = qs('admin-search');
|
||||
const adminPasswordEl = qs('admin-password');
|
||||
const authStatusEl = qs('admin-auth-status');
|
||||
const runsStatusEl = qs('runs-admin-status');
|
||||
const runsTbody = qs('run-admin-body');
|
||||
const runSearchEl = qs('run-search');
|
||||
let items = [];
|
||||
let runs = [];
|
||||
const adminPasswordKey = 'fedl_admin_password';
|
||||
|
||||
function getAdminPassword(){
|
||||
try{return sessionStorage.getItem(adminPasswordKey) || '';}
|
||||
catch(e){return '';}
|
||||
}
|
||||
|
||||
function setAdminPassword(password){
|
||||
try{
|
||||
if(password) sessionStorage.setItem(adminPasswordKey, password);
|
||||
else sessionStorage.removeItem(adminPasswordKey);
|
||||
}catch(e){}
|
||||
if(adminPasswordEl) adminPasswordEl.value = password;
|
||||
if(authStatusEl){
|
||||
authStatusEl.textContent = password
|
||||
? 'Password saved for this browser session.'
|
||||
: 'Saved only in this browser session.';
|
||||
authStatusEl.classList.remove('error-text');
|
||||
}
|
||||
}
|
||||
|
||||
function authHeaders(extraHeaders){
|
||||
const headers = Object.assign({}, extraHeaders || {});
|
||||
const password = getAdminPassword();
|
||||
if(password){
|
||||
headers.Authorization = `Basic ${btoa(`fedl:${password}`)}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
function handleAdminAuthFailure(message, targetSetter){
|
||||
setAdminPassword('');
|
||||
targetSetter(message || 'Admin password required or incorrect.', true);
|
||||
}
|
||||
|
||||
function setStatus(message, isError){
|
||||
if(!statusEl) return;
|
||||
@@ -627,15 +663,20 @@
|
||||
normalizePositions();
|
||||
return fetch(liveApiUrl, {
|
||||
method:'PUT',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
headers:authHeaders({'Content-Type':'application/json'}),
|
||||
body: JSON.stringify({text: formatData(items)})
|
||||
}).then(r=>{
|
||||
if(r.status === 401) throw new Error('Admin auth failed');
|
||||
if(!r.ok) throw new Error('Save failed');
|
||||
clearItemsCache();
|
||||
renderAdminTable();
|
||||
setStatus('Saved. Live pages update automatically.');
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
if(String(err && err.message || '') === 'Admin auth failed'){
|
||||
handleAdminAuthFailure('Wrong admin password. Enter it above, then try again.', setStatus);
|
||||
return;
|
||||
}
|
||||
setStatus('Could not save. Check the live server endpoint.', true);
|
||||
});
|
||||
}
|
||||
@@ -680,7 +721,7 @@
|
||||
if(reviewNotes === null) return;
|
||||
fetch(`${liveRunsUrl}/${encodeURIComponent(runId)}`, {
|
||||
method:'PUT',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
headers:authHeaders({'Content-Type':'application/json'}),
|
||||
body: JSON.stringify({
|
||||
...run,
|
||||
status,
|
||||
@@ -688,12 +729,17 @@
|
||||
reviewedBy:'FEDL Admin'
|
||||
})
|
||||
}).then(r=>{
|
||||
if(r.status === 401) throw new Error('Admin auth failed');
|
||||
if(!r.ok) throw new Error('Run update failed');
|
||||
clearRunsCache();
|
||||
setRunsStatus(`Run marked ${status}.`);
|
||||
return refreshRuns();
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
if(String(err && err.message || '') === 'Admin auth failed'){
|
||||
handleAdminAuthFailure('Wrong admin password. Enter it above to review runs.', setRunsStatus);
|
||||
return;
|
||||
}
|
||||
setRunsStatus('Could not update that run.', true);
|
||||
});
|
||||
}
|
||||
@@ -704,14 +750,20 @@
|
||||
return;
|
||||
}
|
||||
fetch(`${liveRunsUrl}/${encodeURIComponent(runId)}`, {
|
||||
method:'DELETE'
|
||||
method:'DELETE',
|
||||
headers:authHeaders()
|
||||
}).then(r=>{
|
||||
if(r.status === 401) throw new Error('Admin auth failed');
|
||||
if(!r.ok) throw new Error('Run delete failed');
|
||||
clearRunsCache();
|
||||
setRunsStatus('Run removed from the queue.');
|
||||
return refreshRuns();
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
if(String(err && err.message || '') === 'Admin auth failed'){
|
||||
handleAdminAuthFailure('Wrong admin password. Enter it above to delete runs.', setRunsStatus);
|
||||
return;
|
||||
}
|
||||
setRunsStatus('Could not delete that run.', true);
|
||||
});
|
||||
}
|
||||
@@ -785,6 +837,12 @@
|
||||
if(runSearchEl){
|
||||
runSearchEl.addEventListener('input', renderRunsTable);
|
||||
}
|
||||
if(adminPasswordEl){
|
||||
setAdminPassword(getAdminPassword());
|
||||
adminPasswordEl.addEventListener('input', function(){
|
||||
setAdminPassword(adminPasswordEl.value.trim());
|
||||
});
|
||||
}
|
||||
|
||||
bindLiveUpdates();
|
||||
onLiveUpdate(function(updatedItems){
|
||||
|
||||
+5
-2
@@ -37,12 +37,15 @@ table.levels-table tr:hover td{background:rgba(255,255,255,0.01)}
|
||||
.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}
|
||||
.admin-password-field span{font-size:.82rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
||||
.admin-password-field input,.admin-actions input[type=text],.admin-actions input[type=password]{min-width:220px;padding:10px 12px;border-radius:10px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.03);color:var(--text)}
|
||||
.admin-toolbar{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;margin-bottom:18px}
|
||||
.admin-toolbar h2{margin:0 0 8px 0}
|
||||
.admin-actions{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
|
||||
.admin-actions input[type=text]{min-width:220px;padding:10px 12px;border-radius:10px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.03);color:var(--text)}
|
||||
.admin-table td:last-child{text-align:center}
|
||||
.run-admin-cell{display:grid;gap:6px}
|
||||
.run-admin-actions{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end}
|
||||
@@ -256,5 +259,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-actions input[type=text]{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}}
|
||||
@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