← back to Dandeana Hub
server.js
90 lines
// Dandeana Hub — private client news hub for the Larchmont Street Subdivision (Poway)
// Whole-site HTTP Basic Auth: every route 401s without creds except /healthz (pm2/deploy smoke).
const express = require('express');
const fs = require('fs');
const path = require('path');
const { runRefresh } = require('./scripts/refresh');
const { pullParcels } = require('./scripts/pull-parcels');
const PORT = process.env.PORT || 9917;
const DATA = path.join(__dirname, 'data');
// BASIC_AUTH="user:pass" overrides; default is the client credential pair.
// Split on the FIRST colon only so passwords containing colons are preserved.
const _rawAuth = process.env.BASIC_AUTH || 'dandeana:w3tEgaYeOPDQ';
const _authColon = _rawAuth.indexOf(':');
const AUTH_USER = _rawAuth.slice(0, _authColon);
const AUTH_PASS = _rawAuth.slice(_authColon + 1);
// Second accepted pair: Steve's unified admin credential (ADMIN_AUTH="user:pass" overrides).
// The client pair above must keep working — never collapse these into one.
const _rawAdmin = process.env.ADMIN_AUTH || 'admin:DW2024!';
const _admColon = _rawAdmin.indexOf(':');
const ADMIN_USER = _rawAdmin.slice(0, _admColon);
const ADMIN_PASS = _rawAdmin.slice(_admColon + 1);
const app = express();
app.disable('x-powered-by');
app.get('/healthz', (_req, res) => res.json({ ok: true, service: 'dandeana-hub' }));
app.use((req, res, next) => {
const hdr = req.headers.authorization || '';
const [scheme, b64] = hdr.split(' ');
if (scheme === 'Basic' && b64) {
const decoded = Buffer.from(b64, 'base64').toString();
const ci = decoded.indexOf(':');
const u = decoded.slice(0, ci), p = decoded.slice(ci + 1);
if (u === AUTH_USER && p === AUTH_PASS) return next();
if (u === ADMIN_USER && p === ADMIN_PASS) return next();
}
res.set('WWW-Authenticate', 'Basic realm="Dandeana Private Hub"');
return res.status(401).send('Authentication required');
});
app.use(express.static(path.join(__dirname, 'public')));
app.use('/docs', express.static(path.join(__dirname, 'docs')));
function readJson(file, fallback) {
try { return JSON.parse(fs.readFileSync(path.join(DATA, file), 'utf8')); }
catch { return fallback; }
}
function readItems() {
try {
return fs.readFileSync(path.join(DATA, 'news.jsonl'), 'utf8')
.split('\n').filter(Boolean).map(l => JSON.parse(l));
} catch { return []; }
}
app.get('/api/project', (_req, res) => res.json(readJson('project.json', {})));
app.get('/api/timeline', (_req, res) => res.json(readJson('timeline.json', [])));
app.get('/api/documents', (_req, res) => res.json(readJson('documents.json', [])));
app.get('/api/records', (_req, res) => res.json(readJson('records.json', [])));
app.get('/api/parcels', (_req, res) => res.json(readJson('parcels.json', { lots: [] })));
app.get('/api/items', (_req, res) => {
const meta = readJson('refresh-meta.json', {});
res.json({ lastRefresh: meta.lastRefresh || null, items: readItems() });
});
let refreshing = false;
app.post('/api/refresh', async (_req, res) => {
if (refreshing) return res.status(409).json({ ok: false, error: 'refresh already running' });
refreshing = true;
try {
const result = await runRefresh(DATA);
res.json({ ok: true, ...result });
} catch (e) {
res.status(500).json({ ok: false, error: String(e.message || e) });
} finally { refreshing = false; }
});
app.listen(PORT, '127.0.0.1', () => console.log(`dandeana-hub listening on 127.0.0.1:${PORT}`));
// Daily self-refresh so the hub stays current with zero external schedulers.
const DAY = 24 * 60 * 60 * 1000;
const BOOT_DELAY = 15 * 1000; // wait for process to fully settle before first pull
function dailyPull() {
runRefresh(DATA).catch(e => console.error('scheduled refresh failed:', e.message));
pullParcels(DATA).catch(e => console.error('parcel pull failed:', e.message));
}
setInterval(dailyPull, DAY);
setTimeout(dailyPull, BOOT_DELAY);