did a roll backe do to problems
This commit is contained in:
@@ -12,15 +12,9 @@ GD FEDL is a Geometry Dash list site built as a multi-page static frontend with
|
||||
- `guess.html` rank guessing game
|
||||
- `roulette.html` demon roulette picker
|
||||
- `rules.html` rules page
|
||||
- `about.html` about page
|
||||
- `signup.html` and `login.html` account pages
|
||||
- `account.html` account settings page
|
||||
- `reset-password.html` password reset page
|
||||
- `post.html` and `messages.html` community features
|
||||
- `contact.html` bug report / contact page
|
||||
- `support.html` support page
|
||||
- `sitemap.html` sitemap page
|
||||
- `offlineindex.html` offline fallback page
|
||||
- `errors.html` plus standalone HTTP-style error pages
|
||||
|
||||
## Tech Stack
|
||||
|
||||
+11
-44
@@ -28,16 +28,24 @@
|
||||
<section id="admin-login-screen" class="admin-login-screen">
|
||||
<div class="admin-login-card">
|
||||
<p class="hero-kicker">Admin Access</p>
|
||||
<h2>Enter admin password</h2>
|
||||
<p class="muted">Enter the server admin password to access the admin panel.</p>
|
||||
<h2>Log in with your account</h2>
|
||||
<p class="muted">Enter your username, account password, and admin password to access the admin panel.</p>
|
||||
<form id="admin-login-form" class="admin-login-form">
|
||||
<label class="admin-password-field" for="admin-username">
|
||||
<span>Username</span>
|
||||
<input id="admin-username" type="text" placeholder="Enter your username" autocomplete="username" />
|
||||
</label>
|
||||
<label class="admin-password-field" for="admin-account-password">
|
||||
<span>Account password</span>
|
||||
<input id="admin-account-password" type="password" placeholder="Enter your account password" autocomplete="current-password" />
|
||||
</label>
|
||||
<label class="admin-password-field" for="admin-password">
|
||||
<span>Admin password</span>
|
||||
<input id="admin-password" type="password" placeholder="Enter server admin 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"></p>
|
||||
<p id="admin-auth-status" class="muted">Saved only in this browser session.</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -48,7 +56,6 @@
|
||||
<button type="button" class="admin-tab active" data-tab="stats">Stats</button>
|
||||
<button type="button" class="admin-tab" data-tab="list">List Editor</button>
|
||||
<button type="button" class="admin-tab" data-tab="runs">Run Queue</button>
|
||||
<button type="button" class="admin-tab" data-tab="mods">Mods</button>
|
||||
<button type="button" class="admin-tab" data-tab="bugs">Bug Reports</button>
|
||||
<button type="button" class="admin-tab" data-tab="import">Import</button>
|
||||
</section>
|
||||
@@ -143,46 +150,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel admin-panel admin-tab-panel" id="tab-mods" hidden>
|
||||
<div class="admin-toolbar">
|
||||
<div>
|
||||
<p class="hero-kicker">Mod Management</p>
|
||||
<h2>Manage moderators</h2>
|
||||
<p id="mods-admin-status" class="muted">Loading mods...</p>
|
||||
</div>
|
||||
<div class="admin-actions">
|
||||
<button id="add-mod-btn" type="button" class="btn">Add Mod</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="levels-table admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th style="width:200px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="mods-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="add-mod-modal" class="video-modal" hidden>
|
||||
<div class="inner modal-card" role="dialog" aria-modal="true" aria-labelledby="add-mod-title">
|
||||
<p class="hero-kicker">Add Mod</p>
|
||||
<h2 id="add-mod-title">Add a new moderator</h2>
|
||||
<form id="add-mod-form" class="admin-login-form">
|
||||
<label class="admin-password-field" for="new-mod-username">
|
||||
<span>Username</span>
|
||||
<input id="new-mod-username" type="text" placeholder="Enter username" required />
|
||||
</label>
|
||||
<div class="admin-login-actions">
|
||||
<button type="submit" class="btn">Add Mod</button>
|
||||
<button type="button" class="btn ghost-btn" id="add-mod-cancel">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel admin-panel admin-tab-panel" id="tab-bugs" hidden>
|
||||
<div class="admin-toolbar">
|
||||
<div>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
const liveRunsUrl = `${liveServerBase}/api/runs`;
|
||||
const liveEventsUrl = `${liveServerBase}/events`;
|
||||
const liveDataFileUrl = `${liveServerBase}/server/data.txt`;
|
||||
const MOD_USERS = ['wolf_reaper90'];
|
||||
/** Use for POST /api/import/* and any path under the same base as list/runs (not root-relative /api/...). */
|
||||
function liveApiPath(path){
|
||||
const p = String(path || '').startsWith('/') ? path : `/${path}`;
|
||||
@@ -369,63 +370,53 @@
|
||||
nav.appendChild(wrap);
|
||||
}
|
||||
|
||||
function isFedlMod(){
|
||||
if(!fedlServerUsername || !canUseLiveServer) return Promise.resolve(false);
|
||||
const token = fedlGetAuthToken();
|
||||
if(!token) return Promise.resolve(false);
|
||||
return fetch(liveApiPath('/api/modcheck'), {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
}).then(r=>r.ok ? r.json() : Promise.resolve({isMod:false})).then(d=>d.isMod||false).catch(()=>false);
|
||||
function fedlUpdateAuthNav(){
|
||||
const wrap = document.querySelector('.fedl-auth-nav');
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
|
||||
function fedlUpdateAuthNav(){
|
||||
const wrap = document.querySelector('.fedl-auth-nav');
|
||||
if (!wrap) {
|
||||
return;
|
||||
wrap.textContent = '';
|
||||
if (fedlServerUsername) {
|
||||
const isMod = MOD_USERS.includes(fedlServerUsername.toLowerCase());
|
||||
const label = document.createElement('span');
|
||||
label.className = 'fedl-auth-label muted';
|
||||
label.appendChild(document.createTextNode('Hi, '));
|
||||
const strong = document.createElement('strong');
|
||||
strong.textContent = fedlServerUsername;
|
||||
label.appendChild(strong);
|
||||
wrap.appendChild(label);
|
||||
wrap.appendChild(document.createTextNode(' '));
|
||||
if (isMod) {
|
||||
const adminLink = document.createElement('a');
|
||||
adminLink.href = 'admelist.html';
|
||||
adminLink.textContent = 'Admin';
|
||||
wrap.appendChild(adminLink);
|
||||
wrap.appendChild(document.createTextNode(' '));
|
||||
}
|
||||
wrap.textContent = '';
|
||||
if (fedlServerUsername) {
|
||||
isFedlMod().then(isMod=>{
|
||||
const label = document.createElement('span');
|
||||
label.className = 'fedl-auth-label muted';
|
||||
label.appendChild(document.createTextNode('Hi, '));
|
||||
const strong = document.createElement('strong');
|
||||
strong.textContent = fedlServerUsername;
|
||||
label.appendChild(strong);
|
||||
wrap.appendChild(label);
|
||||
wrap.appendChild(document.createTextNode(' '));
|
||||
if (isMod) {
|
||||
const adminLink = document.createElement('a');
|
||||
adminLink.href = 'admelist.html';
|
||||
adminLink.textContent = 'Admin';
|
||||
wrap.appendChild(adminLink);
|
||||
wrap.appendChild(document.createTextNode(' '));
|
||||
}
|
||||
const accountLink = document.createElement('a');
|
||||
accountLink.href = 'account.html';
|
||||
accountLink.textContent = 'Account';
|
||||
wrap.appendChild(accountLink);
|
||||
wrap.appendChild(document.createTextNode(' '));
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn ghost-btn small-btn fedl-logout-btn';
|
||||
btn.textContent = 'Log out';
|
||||
btn.addEventListener('click', ()=>{
|
||||
const tok = fedlGetAuthToken();
|
||||
if (tok && canUseLiveServer) {
|
||||
fetch(`${liveServerBase}/api/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${tok}` }
|
||||
}).catch(()=>{});
|
||||
}
|
||||
fedlClearServerSession();
|
||||
fedlUpdateAuthNav();
|
||||
document.dispatchEvent(new CustomEvent('fedl-auth-updated'));
|
||||
window.location.reload();
|
||||
});
|
||||
wrap.appendChild(btn);
|
||||
});
|
||||
} else {
|
||||
const accountLink = document.createElement('a');
|
||||
accountLink.href = 'account.html';
|
||||
accountLink.textContent = 'Account';
|
||||
wrap.appendChild(accountLink);
|
||||
wrap.appendChild(document.createTextNode(' '));
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn ghost-btn small-btn fedl-logout-btn';
|
||||
btn.textContent = 'Log out';
|
||||
btn.addEventListener('click', ()=>{
|
||||
const tok = fedlGetAuthToken();
|
||||
if (tok && canUseLiveServer) {
|
||||
fetch(`${liveServerBase}/api/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${tok}` }
|
||||
}).catch(()=>{});
|
||||
}
|
||||
fedlClearServerSession();
|
||||
fedlUpdateAuthNav();
|
||||
document.dispatchEvent(new CustomEvent('fedl-auth-updated'));
|
||||
window.location.reload();
|
||||
});
|
||||
wrap.appendChild(btn);
|
||||
} else {
|
||||
const a1 = document.createElement('a');
|
||||
const returnTo = encodeURIComponent(window.location.href);
|
||||
a1.href = 'login.html?return=' + returnTo;
|
||||
@@ -1738,6 +1729,8 @@ function isFedlMod(){
|
||||
const addRowBottomBtn = qs('add-row-bottom');
|
||||
const saveBtn = qs('save-list');
|
||||
const searchEl = qs('admin-search');
|
||||
const adminUsernameEl = qs('admin-username');
|
||||
const adminAccountPasswordEl = qs('admin-account-password');
|
||||
const adminPasswordEl = qs('admin-password');
|
||||
const authStatusEl = qs('admin-auth-status');
|
||||
const runsStatusEl = qs('runs-admin-status');
|
||||
@@ -1769,9 +1762,9 @@ function isFedlMod(){
|
||||
|
||||
function getAdminSession(){
|
||||
try{
|
||||
const stored = sessionStorage.getItem(adminPasswordKey);
|
||||
const stored = sessionStorage.getItem(adminSessionKey);
|
||||
if(stored){
|
||||
return { adminPassword: stored };
|
||||
return JSON.parse(stored);
|
||||
}
|
||||
}catch(e){}
|
||||
return null;
|
||||
@@ -1779,10 +1772,8 @@ function isFedlMod(){
|
||||
|
||||
function setAdminSession(session){
|
||||
try{
|
||||
if(session && session.adminPassword){
|
||||
sessionStorage.setItem(adminPasswordKey, session.adminPassword);
|
||||
}else{
|
||||
sessionStorage.removeItem(adminPasswordKey);
|
||||
if(session){
|
||||
sessionStorage.setItem(adminSessionKey, JSON.stringify(session));
|
||||
}else{
|
||||
sessionStorage.removeItem(adminSessionKey);
|
||||
}
|
||||
@@ -1834,9 +1825,17 @@ function isFedlMod(){
|
||||
|
||||
function verifyAdminPassword(){
|
||||
const session = getAdminSession();
|
||||
const password = session?.adminPassword || '';
|
||||
if(!password){
|
||||
handleAdminAuthFailure('Enter the admin password to continue.', function(message, isError){
|
||||
if(!session || !session.username || !session.accountPassword || !session.adminPassword){
|
||||
handleAdminAuthFailure('Enter username, account password, and admin password to continue.', function(message, isError){
|
||||
if(!authStatusEl) return;
|
||||
authStatusEl.textContent = message;
|
||||
authStatusEl.classList.toggle('error-text', !!isError);
|
||||
});
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
const username = session.username.toLowerCase();
|
||||
if(!MOD_USERS.includes(username)){
|
||||
handleAdminAuthFailure('You are not authorized to access the admin panel.', function(message, isError){
|
||||
if(!authStatusEl) return;
|
||||
authStatusEl.textContent = message;
|
||||
authStatusEl.classList.toggle('error-text', !!isError);
|
||||
@@ -1844,15 +1843,37 @@ function isFedlMod(){
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
if(authStatusEl){
|
||||
authStatusEl.textContent = 'Verifying...';
|
||||
authStatusEl.textContent = 'Verifying account...';
|
||||
authStatusEl.classList.remove('error-text');
|
||||
}
|
||||
return fetch(`${liveRunsUrl}/__authcheck__`, {
|
||||
method:'DELETE',
|
||||
headers:authHeaders()
|
||||
return fetch(liveApiPath('/api/auth/login'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: session.username, password: session.accountPassword })
|
||||
}).then(r=>{
|
||||
if(r.status === 401) throw new Error('Wrong password');
|
||||
if(r.status !== 404 && r.status !== 204) throw new Error('Verify failed');
|
||||
if(!r.ok){
|
||||
throw new Error('Invalid account password');
|
||||
}
|
||||
return r.json();
|
||||
}).then(data=>{
|
||||
const token = data.data?.token;
|
||||
if(!token){
|
||||
throw new Error('Invalid account password');
|
||||
}
|
||||
fedlSetAuthToken(token);
|
||||
fedlServerUserId = data.data.userId;
|
||||
fedlServerUsername = data.data.username;
|
||||
document.dispatchEvent(new CustomEvent('fedl-auth-updated'));
|
||||
if(authStatusEl){
|
||||
authStatusEl.textContent = 'Verifying admin access...';
|
||||
}
|
||||
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();
|
||||
@@ -1862,10 +1883,16 @@ function isFedlMod(){
|
||||
}
|
||||
loadAdmin();
|
||||
loadRunsAdmin();
|
||||
return true;
|
||||
return ok;
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
handleAdminAuthFailure('Wrong admin password. Try again.', function(message, isError){
|
||||
let msg = 'Wrong admin password. Try again.';
|
||||
if(err.message === 'Invalid account password'){
|
||||
msg = 'Invalid account password. Try again.';
|
||||
}else if(err.message === 'You are not authorized to access the admin panel.'){
|
||||
msg = 'You are not authorized to access the admin panel.';
|
||||
}
|
||||
handleAdminAuthFailure(msg, function(message, isError){
|
||||
if(!authStatusEl) return;
|
||||
authStatusEl.textContent = message;
|
||||
authStatusEl.classList.toggle('error-text', !!isError);
|
||||
@@ -2490,21 +2517,17 @@ function isFedlMod(){
|
||||
if(key === '?' && !event.ctrlKey && !event.metaKey){
|
||||
window.location.href = 'rules.html';
|
||||
}
|
||||
if(key === 'w' && event.shiftKey && !event.ctrlKey && !event.metaKey){
|
||||
window.location.href = 'admelist.html';
|
||||
}
|
||||
if(key === 'e' && event.shiftKey && !event.ctrlKey && !event.metaKey && page === 'index'){
|
||||
window.location.href = 'admelist.html';
|
||||
}
|
||||
});
|
||||
|
||||
if(loginFormEl){
|
||||
loginFormEl.addEventListener('submit', function(event){
|
||||
event.preventDefault();
|
||||
const username = (adminUsernameEl && adminUsernameEl.value || '').trim();
|
||||
const accountPassword = (adminAccountPasswordEl && adminAccountPasswordEl.value || '').trim();
|
||||
const adminPassword = (adminPasswordEl && adminPasswordEl.value || '').trim();
|
||||
setAdminSession({ adminPassword });
|
||||
setAdminSession({ username, accountPassword, adminPassword });
|
||||
if(authStatusEl){
|
||||
authStatusEl.textContent = 'Verifying...';
|
||||
authStatusEl.textContent = 'Checking credentials...';
|
||||
authStatusEl.classList.remove('error-text');
|
||||
}
|
||||
verifyAdminPassword();
|
||||
@@ -2741,132 +2764,6 @@ function isFedlMod(){
|
||||
});
|
||||
}
|
||||
|
||||
const modsBody = qs('mods-body');
|
||||
const modsAdminStatusEl = qs('mods-admin-status');
|
||||
const addModBtn = qs('add-mod-btn');
|
||||
const addModModal = qs('add-mod-modal');
|
||||
const addModForm = qs('add-mod-form');
|
||||
const addModCancelBtn = qs('add-mod-cancel');
|
||||
let mods = [];
|
||||
|
||||
function setModsStatus(message, isError){
|
||||
if(!modsAdminStatusEl) return;
|
||||
modsAdminStatusEl.textContent = message;
|
||||
modsAdminStatusEl.classList.toggle('error-text', !!isError);
|
||||
}
|
||||
|
||||
function loadMods(){
|
||||
return fetch(liveApiPath('/api/mods'), {
|
||||
headers: authHeaders()
|
||||
}).then(r=>{
|
||||
if(r.status === 401) throw new Error('Auth required');
|
||||
if(!r.ok) throw new Error('Failed to load mods');
|
||||
return r.json();
|
||||
}).then(data=>{
|
||||
mods = data.mods || [];
|
||||
return mods;
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
setModsStatus('Could not load mods.', true);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
function renderModsTable(){
|
||||
if(!modsBody) return;
|
||||
if(!mods.length){
|
||||
modsBody.innerHTML = '<tr><td colspan="2" class="muted">No mods found.</td></tr>';
|
||||
return;
|
||||
}
|
||||
modsBody.innerHTML = mods.map(mod=>`
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(mod)}</strong></td>
|
||||
<td>
|
||||
<button type="button" class="btn danger-btn small-btn" data-mod-remove="${escapeAttr(mod)}">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function refreshMods(){
|
||||
return loadMods().then(loaded=>{
|
||||
mods = loaded;
|
||||
renderModsTable();
|
||||
setModsStatus('Mods loaded.');
|
||||
});
|
||||
}
|
||||
|
||||
if(addModBtn){
|
||||
addModBtn.addEventListener('click', ()=>{
|
||||
if(addModModal) addModModal.hidden = false;
|
||||
const input = qs('new-mod-username');
|
||||
if(input) input.value = '';
|
||||
if(input) input.focus();
|
||||
});
|
||||
}
|
||||
|
||||
if(addModCancelBtn){
|
||||
addModCancelBtn.addEventListener('click', ()=>{
|
||||
if(addModModal) addModModal.hidden = true;
|
||||
});
|
||||
}
|
||||
|
||||
if(addModForm){
|
||||
addModForm.addEventListener('submit', (e)=>{
|
||||
e.preventDefault();
|
||||
const username = qs('new-mod-username').value.trim();
|
||||
if(!username) return;
|
||||
setModsStatus('Adding mod...');
|
||||
fetch(liveApiPath('/api/mods'), {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({ username: username })
|
||||
}).then(r=>{
|
||||
if(r.status === 401) throw new Error('Auth required');
|
||||
if(!r.ok) return r.json().then(err=>{throw new Error(err.error||'Failed');});
|
||||
return r.json();
|
||||
}).then(data=>{
|
||||
mods = data.mods || [];
|
||||
renderModsTable();
|
||||
setModsStatus('Mod added successfully.');
|
||||
if(addModModal) addModModal.hidden = true;
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
setModsStatus(err.message || 'Could not add mod.', true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if(modsBody){
|
||||
modsBody.addEventListener('click', (e)=>{
|
||||
const removeBtn = e.target.closest('[data-mod-remove]');
|
||||
if(!removeBtn) return;
|
||||
const username = removeBtn.dataset.modRemove;
|
||||
if(!username || !confirm(`Remove ${username} as mod?`)) return;
|
||||
setModsStatus('Removing mod...');
|
||||
fetch(liveApiPath('/api/mods'), {
|
||||
method: 'DELETE',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({ username: username })
|
||||
}).then(r=>{
|
||||
if(r.status === 401) throw new Error('Auth required');
|
||||
if(!r.ok) return r.json().then(err=>{throw new Error(err.error||'Failed');});
|
||||
return r.json();
|
||||
}).then(data=>{
|
||||
mods = data.mods || [];
|
||||
renderModsTable();
|
||||
setModsStatus('Mod removed successfully.');
|
||||
}).catch(err=>{
|
||||
console.error(err);
|
||||
setModsStatus(err.message || 'Could not remove mod.', true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if(modsBody || addModBtn){
|
||||
refreshMods();
|
||||
}
|
||||
|
||||
onLiveUpdate(function(updatedItems){
|
||||
items = updatedItems.slice().sort((a,b)=>(Number(a.position) || 0) - (Number(b.position) || 0)).map(item=>({
|
||||
level: item.level,
|
||||
|
||||
+6
-74
@@ -62,10 +62,6 @@ Responses to API routes set permissive CORS headers (`Access-Control-Allow-Origi
|
||||
| `sessions.json` | Bearer session tokens + expiry |
|
||||
| `userdata.json` | Per-user synced state (roulette, list %, saved runs, roulette slots) |
|
||||
| `reset_tokens.json` | One-time password reset tokens + expiry |
|
||||
| `mods.json` | List of moderator usernames |
|
||||
| `bugreports.json` | Bug reports submitted via contact form |
|
||||
| `messages.json` | Private messages between users |
|
||||
| `config.json` | Server configuration (e.g., Discord webhook URL) |
|
||||
|
||||
These files are created on demand where noted below. Use `.gitignore` for `users.json`, `sessions.json`, and `userdata.json` if they contain real data.
|
||||
|
||||
@@ -92,7 +88,7 @@ Unless noted, bodies are **JSON** with `Content-Type: application/json`. Errors
|
||||
|
||||
#### `PUT /api/list`
|
||||
|
||||
- **Auth:** Admin Basic or Mod Bearer (if `ADMIN_PASSWORD` is set)
|
||||
- **Auth:** Admin Basic (if `ADMIN_PASSWORD` is set)
|
||||
- **Body:** `{ "text": "full data.txt content" }`
|
||||
- **200:** `{ "ok": true }` — writes `data.txt`, emits `list-update`
|
||||
|
||||
@@ -113,40 +109,16 @@ Run objects include (among others): `id`, `playerName`, `levelTitle`, `videoUrl`
|
||||
|
||||
#### `PUT /api/runs/:id` / `DELETE /api/runs/:id`
|
||||
|
||||
- **Auth:** Admin Basic or Mod Bearer
|
||||
- **Auth:** Admin Basic
|
||||
- **PUT body:** fields merged via `normalizeRun` with existing run
|
||||
200 / 404 / 400 as appropriate; emits `runs-update` on success
|
||||
|
||||
#### `POST /api/runs/bulk-approve`
|
||||
|
||||
- **Auth:** Admin Basic or Mod Bearer
|
||||
- **Auth:** Admin Basic
|
||||
- **Body:** `{ "playerName": "exact match", "reviewNotes?": "..." }`
|
||||
- **200:** `{ "ok": true, "approved": number, "playerName": "..." }` — approves all **pending** runs whose `playerName` matches (case-insensitive); may emit `runs-update`
|
||||
|
||||
### Mod management
|
||||
|
||||
#### `GET /api/mods`
|
||||
|
||||
- **Auth:** Mod Bearer
|
||||
- **200:** `{ "mods": [ "username", ... ] }`
|
||||
|
||||
#### `POST /api/mods`
|
||||
|
||||
- **Auth:** Mod Bearer
|
||||
- **Body:** `{ "username": "newmod" }`
|
||||
- **201:** `{ "ok": true, "mods": [ ... ] }` — adds user to mods list
|
||||
|
||||
#### `DELETE /api/mods`
|
||||
|
||||
- **Auth:** Mod Bearer
|
||||
- **Body:** `{ "username": "oldmod" }`
|
||||
- **200:** `{ "ok": true, "mods": [ ... ] }` — removes user from mods list
|
||||
|
||||
#### `GET /api/modcheck`
|
||||
|
||||
- **Auth:** Basic or Bearer
|
||||
- **200:** `{ "isMod": true/false, "username": "..." }` — checks if authenticated user is a mod
|
||||
|
||||
### Auth (FEDL user accounts)
|
||||
|
||||
#### `POST /api/auth/signup`
|
||||
@@ -222,64 +194,24 @@ Run objects include (among others): `id`, `playerName`, `levelTitle`, `videoUrl`
|
||||
- **Body:** `{ "data": { ...same fields as above... } }` — server sanitizes `savedRuns` and `rouletteSlots` (size limits and field trimming)
|
||||
- **200:** `{ "ok": true }`
|
||||
- **401 / 400**
|
||||
### Bug reports
|
||||
|
||||
#### `GET /api/bugreports`
|
||||
|
||||
- **Auth:** Admin Basic or Mod Bearer
|
||||
- **200:** `{ "items": [ bugreport, ... ] }`
|
||||
|
||||
#### `POST /api/bugreports`
|
||||
|
||||
- **Body:** `{ "category", "subject", "description", "email?" }`
|
||||
- **201:** `{ "ok": true, "item": bugreport }` — emits `bugreports-update`
|
||||
|
||||
#### `PUT /api/bugreports/:id` / `DELETE /api/bugreports/:id`
|
||||
|
||||
- **Auth:** Admin Basic or Mod Bearer
|
||||
- **PUT body:** fields to update (category, subject, description, email, status)
|
||||
- 200 / 404 / 400; emits `bugreports-update` on success
|
||||
|
||||
### Messages
|
||||
|
||||
#### `GET /api/messages`
|
||||
|
||||
- **Header:** `Authorization: Bearer <token>`
|
||||
- **200:** `{ "items": [ message, ... ] }` — user's messages
|
||||
|
||||
#### `POST /api/messages`
|
||||
|
||||
- **Header:** `Authorization: Bearer <token>`
|
||||
- **Body:** `{ "toUsername", "content" }`
|
||||
- **201:** `{ "ok": true, "item": message }` — emits `messages-update` to recipient
|
||||
|
||||
#### `GET /api/messages/conversation?with=username`
|
||||
|
||||
- **Header:** `Authorization: Bearer <token>`
|
||||
- **200:** `{ "items": [ message, ... ] }` — conversation with specified user
|
||||
|
||||
#### `POST /api/messages/search`
|
||||
|
||||
- **Header:** `Authorization: Bearer <token>`
|
||||
- **Body:** `{ "query": "username prefix" }`
|
||||
- **200:** `{ "items": [ { "username", "userId" }, ... ] }` — user search results
|
||||
### Imports (admin + external APIs)
|
||||
|
||||
#### `POST /api/import/pointercrate`
|
||||
|
||||
- **Auth:** Admin Basic or Mod Bearer
|
||||
- **Auth:** Admin Basic
|
||||
- Fetches Pointercrate records, maps to run shape, appends to `runs.json`
|
||||
- **200:** summary object with counts (see implementation)
|
||||
|
||||
#### `POST /api/import/aredl`
|
||||
|
||||
- **Auth:** Admin Basic or Mod Bearer
|
||||
- **Auth:** Admin Basic
|
||||
- Requires `AREDL_ACCESS_TOKEN` or `AREDL_API_KEY` configured
|
||||
- **200:** summary / **500** on configuration or API errors
|
||||
|
||||
#### `POST /api/import/targeted`
|
||||
|
||||
- **Auth:** Admin Basic or Mod Bearer
|
||||
- **Auth:** Admin Basic
|
||||
- **Body:** `{ "source": "pointercrate" | "aredl", "filter": "player" | "level", "query": "string" }`
|
||||
- Filters remote records and appends matching runs
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
["wolf_reaper90"]
|
||||
+6
-128
@@ -14,26 +14,8 @@ const userDataPath = path.join(__dirname, 'userdata.json');
|
||||
const resetTokensPath = path.join(__dirname, 'reset_tokens.json');
|
||||
const bugReportsPath = path.join(__dirname, 'bugreports.json');
|
||||
const messagesPath = path.join(__dirname, 'messages.json');
|
||||
const modsPath = path.join(__dirname, 'mods.json');
|
||||
const configPath = path.join(__dirname, 'config.json');
|
||||
|
||||
function readMods() {
|
||||
try {
|
||||
if (fs.existsSync(modsPath)) {
|
||||
const raw = fs.readFileSync(modsPath, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed)) return parsed;
|
||||
}
|
||||
} catch (e) {}
|
||||
return [];
|
||||
}
|
||||
|
||||
function writeMods(mods) {
|
||||
fs.writeFileSync(modsPath, JSON.stringify(mods, null, 2) + '\n', 'utf8');
|
||||
}
|
||||
|
||||
let MOD_USERS = readMods();
|
||||
|
||||
const serverConfig = safeReadJsonFile(configPath, {}, 'config.json');
|
||||
const discordWebhookUrl = serverConfig.discordWebhookUrl || '';
|
||||
|
||||
@@ -171,28 +153,13 @@ function isAuthorized(req) {
|
||||
}
|
||||
}
|
||||
|
||||
function requireMod(req, res) {
|
||||
const token = getBearerToken(req);
|
||||
const sess = findSession(token);
|
||||
if (!sess) {
|
||||
setCors(res);
|
||||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Authentication required' }));
|
||||
return false;
|
||||
}
|
||||
const username = String(sess.username || '').toLowerCase();
|
||||
if (!MOD_USERS.includes(username)) {
|
||||
setCors(res);
|
||||
res.writeHead(403, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Not authorized' }));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function requireAdmin(req, res) {
|
||||
if (isAuthorized(req)) return true;
|
||||
return requireMod(req, res);
|
||||
setCors(res);
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="FEDL Admin"');
|
||||
res.writeHead(401, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end('Authentication required');
|
||||
return false;
|
||||
}
|
||||
|
||||
function ensureRunsFile() {
|
||||
@@ -633,6 +600,7 @@ function safeReadBugReports() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readPosts() {
|
||||
ensurePostsFile();
|
||||
@@ -1728,95 +1696,6 @@ const server = http.createServer((req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'DELETE' && pathname === '/api/runs/__authcheck__') {
|
||||
if (!requireAdmin(req, res)) return;
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && pathname === '/api/modcheck') {
|
||||
let username = '';
|
||||
const authHeader = String(req.headers.authorization || '');
|
||||
if (authHeader.startsWith('Basic ')) {
|
||||
try {
|
||||
const decoded = Buffer.from(authHeader.slice(6), 'base64').toString('utf8');
|
||||
const separatorIndex = decoded.indexOf(':');
|
||||
username = separatorIndex === -1 ? '' : decoded.slice(0, separatorIndex).toLowerCase();
|
||||
} catch (error) {
|
||||
sendJson(res, 400, { error: 'Invalid authorization header' });
|
||||
return;
|
||||
}
|
||||
} else if (authHeader.startsWith('Bearer ')) {
|
||||
const token = authHeader.slice(7);
|
||||
const sess = findSession(token);
|
||||
if (sess) {
|
||||
username = String(sess.username || '').toLowerCase();
|
||||
}
|
||||
}
|
||||
if (!username) {
|
||||
sendJson(res, 401, { error: 'Authorization required' });
|
||||
return;
|
||||
}
|
||||
const isMod = MOD_USERS.includes(username);
|
||||
sendJson(res, 200, { isMod: isMod, username: username });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === '/api/mods') {
|
||||
if (req.method === 'GET') {
|
||||
if (!requireMod(req, res)) return;
|
||||
sendJson(res, 200, { mods: MOD_USERS });
|
||||
return;
|
||||
}
|
||||
if (req.method === 'POST') {
|
||||
if (!requireMod(req, res)) return;
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk; if (body.length > 65536) req.destroy(); });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const payload = JSON.parse(body || '{}');
|
||||
const newMod = String(payload.username || '').trim().toLowerCase();
|
||||
if (!newMod) {
|
||||
sendJson(res, 400, { error: 'username is required' });
|
||||
return;
|
||||
}
|
||||
if (MOD_USERS.includes(newMod)) {
|
||||
sendJson(res, 400, { error: 'User is already a mod' });
|
||||
return;
|
||||
}
|
||||
MOD_USERS.push(newMod);
|
||||
writeMods(MOD_USERS);
|
||||
sendJson(res, 201, { ok: true, mods: MOD_USERS });
|
||||
} catch (e) {
|
||||
sendJson(res, 400, { error: 'Invalid request body' });
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (req.method === 'DELETE') {
|
||||
if (!requireMod(req, res)) return;
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk; if (body.length > 65536) req.destroy(); });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const payload = JSON.parse(body || '{}');
|
||||
const removeMod = String(payload.username || '').trim().toLowerCase();
|
||||
if (!removeMod) {
|
||||
sendJson(res, 400, { error: 'username is required' });
|
||||
return;
|
||||
}
|
||||
MOD_USERS = MOD_USERS.filter(m => m !== removeMod);
|
||||
writeMods(MOD_USERS);
|
||||
sendJson(res, 200, { ok: true, mods: MOD_USERS });
|
||||
} catch (e) {
|
||||
sendJson(res, 400, { error: 'Invalid request body' });
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && pathname === '/api/runs') {
|
||||
let body = '';
|
||||
req.on('data', chunk => {
|
||||
@@ -2171,5 +2050,4 @@ server.listen(port, host, () => {
|
||||
console.log(`Legacy list fallback: ${legacyDataPath}`);
|
||||
console.log(`Using runs file: ${runsPath}`);
|
||||
console.log(`Admin password protection: ${adminPassword ? 'enabled' : 'disabled'}`);
|
||||
console.log(`Mods loaded from mods.json`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user