Implement moderator management system with API endpoints for adding, removing, and checking moderators. Update server and documentation accordingly.

This commit is contained in:
2026-04-14 19:26:16 -05:00
parent f27fcf7bf7
commit 016d998263
6 changed files with 459 additions and 126 deletions
+6
View File
@@ -12,9 +12,15 @@ 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
+44 -11
View File
@@ -28,24 +28,16 @@
<section id="admin-login-screen" class="admin-login-screen">
<div class="admin-login-card">
<p class="hero-kicker">Admin Access</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>
<h2>Enter admin password</h2>
<p class="muted">Enter the server 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">Saved only in this browser session.</p>
<p id="admin-auth-status" class="muted"></p>
</div>
</form>
</div>
@@ -56,6 +48,7 @@
<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>
@@ -150,6 +143,46 @@
</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>
+206 -103
View File
@@ -11,7 +11,6 @@
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}`;
@@ -370,53 +369,63 @@
nav.appendChild(wrap);
}
function fedlUpdateAuthNav(){
const wrap = document.querySelector('.fedl-auth-nav');
if (!wrap) {
return;
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);
}
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(' '));
function fedlUpdateAuthNav(){
const wrap = document.querySelector('.fedl-auth-nav');
if (!wrap) {
return;
}
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 {
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 a1 = document.createElement('a');
const returnTo = encodeURIComponent(window.location.href);
a1.href = 'login.html?return=' + returnTo;
@@ -1729,8 +1738,6 @@
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');
@@ -1762,9 +1769,9 @@
function getAdminSession(){
try{
const stored = sessionStorage.getItem(adminSessionKey);
const stored = sessionStorage.getItem(adminPasswordKey);
if(stored){
return JSON.parse(stored);
return { adminPassword: stored };
}
}catch(e){}
return null;
@@ -1772,8 +1779,10 @@
function setAdminSession(session){
try{
if(session){
sessionStorage.setItem(adminSessionKey, JSON.stringify(session));
if(session && session.adminPassword){
sessionStorage.setItem(adminPasswordKey, session.adminPassword);
}else{
sessionStorage.removeItem(adminPasswordKey);
}else{
sessionStorage.removeItem(adminSessionKey);
}
@@ -1825,17 +1834,9 @@
function verifyAdminPassword(){
const session = getAdminSession();
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){
const password = session?.adminPassword || '';
if(!password){
handleAdminAuthFailure('Enter the admin password to continue.', function(message, isError){
if(!authStatusEl) return;
authStatusEl.textContent = message;
authStatusEl.classList.toggle('error-text', !!isError);
@@ -1843,37 +1844,15 @@
return Promise.resolve(false);
}
if(authStatusEl){
authStatusEl.textContent = 'Verifying account...';
authStatusEl.textContent = 'Verifying...';
authStatusEl.classList.remove('error-text');
}
return fetch(liveApiPath('/api/auth/login'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: session.username, password: session.accountPassword })
return fetch(`${liveRunsUrl}/__authcheck__`, {
method:'DELETE',
headers:authHeaders()
}).then(r=>{
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');
if(r.status === 401) throw new Error('Wrong password');
if(r.status !== 404 && r.status !== 204) throw new Error('Verify failed');
return true;
}).then(ok=>{
unlockAdminShell();
@@ -1883,16 +1862,10 @@
}
loadAdmin();
loadRunsAdmin();
return ok;
return true;
}).catch(err=>{
console.error(err);
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){
handleAdminAuthFailure('Wrong admin password. Try again.', function(message, isError){
if(!authStatusEl) return;
authStatusEl.textContent = message;
authStatusEl.classList.toggle('error-text', !!isError);
@@ -2517,17 +2490,21 @@
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({ username, accountPassword, adminPassword });
setAdminSession({ adminPassword });
if(authStatusEl){
authStatusEl.textContent = 'Checking credentials...';
authStatusEl.textContent = 'Verifying...';
authStatusEl.classList.remove('error-text');
}
verifyAdminPassword();
@@ -2764,6 +2741,132 @@
});
}
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,
+74 -6
View File
@@ -62,6 +62,10 @@ 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.
@@ -88,7 +92,7 @@ Unless noted, bodies are **JSON** with `Content-Type: application/json`. Errors
#### `PUT /api/list`
- **Auth:** Admin Basic (if `ADMIN_PASSWORD` is set)
- **Auth:** Admin Basic or Mod Bearer (if `ADMIN_PASSWORD` is set)
- **Body:** `{ "text": "full data.txt content" }`
- **200:** `{ "ok": true }` — writes `data.txt`, emits `list-update`
@@ -109,16 +113,40 @@ Run objects include (among others): `id`, `playerName`, `levelTitle`, `videoUrl`
#### `PUT /api/runs/:id` / `DELETE /api/runs/:id`
- **Auth:** Admin Basic
- **Auth:** Admin Basic or Mod Bearer
- **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
- **Auth:** Admin Basic or Mod Bearer
- **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`
@@ -194,24 +222,64 @@ 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
- **Auth:** Admin Basic or Mod Bearer
- 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
- **Auth:** Admin Basic or Mod Bearer
- Requires `AREDL_ACCESS_TOKEN` or `AREDL_API_KEY` configured
- **200:** summary / **500** on configuration or API errors
#### `POST /api/import/targeted`
- **Auth:** Admin Basic
- **Auth:** Admin Basic or Mod Bearer
- **Body:** `{ "source": "pointercrate" | "aredl", "filter": "player" | "level", "query": "string" }`
- Filters remote records and appends matching runs
+1
View File
@@ -0,0 +1 @@
["wolf_reaper90"]
+128 -6
View File
@@ -14,8 +14,26 @@ 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 || '';
@@ -153,13 +171,28 @@ 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;
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;
return requireMod(req, res);
}
function ensureRunsFile() {
@@ -600,7 +633,6 @@ function safeReadBugReports() {
return [];
}
}
}
function readPosts() {
ensurePostsFile();
@@ -1696,6 +1728,95 @@ 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 => {
@@ -2050,4 +2171,5 @@ 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`);
});