← back to Ungate Console
server.js
74 lines
#!/usr/bin/env node
// Ungate Console — one page of everything blocked, grouped by who must act.
// Zero-dep, Basic-auth admin/DW2024!, OS-assigned free port.
const http = require('http');
const fs = require('fs');
const path = require('path');
const USER = process.env.UC_USER || 'admin';
const PASS = process.env.UC_PASS || 'DW2024!';
const DIR = __dirname;
function auth(req, res) {
const h = req.headers.authorization || '';
const [, b64] = h.split(' ');
const [u, p] = Buffer.from(b64 || '', 'base64').toString().split(':');
if (u === USER && p === PASS) return true;
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="ungate-console"' });
res.end('auth required');
return false;
}
const DECISIONS = path.join(DIR, 'data', 'decisions.jsonl');
// current decision per tk (last-write-wins) from the append-only log
function decisionMap() {
const m = {};
try {
fs.readFileSync(DECISIONS, 'utf8').split('\n').filter(Boolean).forEach(l => {
try { const d = JSON.parse(l); if (d.tk) m[d.tk] = d; } catch {}
});
} catch {}
return m;
}
const srv = http.createServer((req, res) => {
if (!auth(req, res)) return;
const url = req.url.split('?')[0];
if (url === '/api/items') {
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(fs.readFileSync(path.join(DIR, 'data', 'items.json')));
}
if (url === '/api/decisions') {
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify(decisionMap()));
}
if (url === '/api/decide' && req.method === 'POST') {
let body = '';
req.on('data', c => { body += c; if (body.length > 1e5) req.destroy(); });
req.on('end', () => {
let d; try { d = JSON.parse(body); } catch { res.writeHead(400); return res.end('bad json'); }
if (!d.tk || !d.decision) { res.writeHead(400); return res.end('need tk+decision'); }
const rec = { tk: d.tk, decision: d.decision, note: (d.note || '').slice(0, 500), ts: new Date().toISOString() };
fs.appendFileSync(DECISIONS, JSON.stringify(rec) + '\n');
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, rec }));
});
return;
}
if (url === '/' || url === '/index.html') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(fs.readFileSync(path.join(DIR, 'index.html')));
}
res.writeHead(404); res.end('not found');
});
let want = Number(process.env.UC_PORT) || 0;
if (!want) { try { want = Number(fs.readFileSync(path.join(DIR, '.port'), 'utf8').trim()) || 0; } catch {} }
srv.on('error', e => { if (e.code === 'EADDRINUSE' && want) { want = 0; srv.listen(0, '127.0.0.1'); } });
srv.listen(want, '127.0.0.1', () => {
const port = srv.address().port;
fs.writeFileSync(path.join(DIR, '.port'), String(port));
console.log(`Ungate Console → http://127.0.0.1:${port} (admin/DW2024!)`);
});