mirror of
https://github.com/mileswolfallen2/miles-web.git
synced 2026-09-08 11:23:17 +00:00
128 lines
3.7 KiB
JavaScript
128 lines
3.7 KiB
JavaScript
const express = require('express');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
// NOTE: set ADMIN_TOKEN to a secret in your environment before running in production.
|
|
// Example: ADMIN_TOKEN="s3cret" node server.js
|
|
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || 'change-me';
|
|
|
|
const DATA_DIR = path.join(__dirname, 'data');
|
|
const DATA_FILE = path.join(DATA_DIR, 'data.json');
|
|
|
|
let DB = loadData();
|
|
|
|
function loadData() {
|
|
try {
|
|
const raw = fs.readFileSync(DATA_FILE, 'utf8');
|
|
return JSON.parse(raw);
|
|
} catch (e) {
|
|
const defaults = {
|
|
profile: {},
|
|
timezone: { id: 'America/Chicago', label: 'Central Time (MN, USA)' },
|
|
now: [],
|
|
posts: []
|
|
};
|
|
saveData(defaults);
|
|
return defaults;
|
|
}
|
|
}
|
|
|
|
function saveData(db) {
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
fs.writeFileSync(DATA_FILE, JSON.stringify(db, null, 2), 'utf8');
|
|
}
|
|
|
|
function mustAuth(req) {
|
|
const header = (req.headers.authorization || '').replace(/^Bearer\s+/i, '');
|
|
return header && safeEqual(header, ADMIN_TOKEN);
|
|
}
|
|
|
|
function safeEqual(a, b) {
|
|
const ha = crypto.createHash('sha256').update(String(a)).digest();
|
|
const hb = crypto.createHash('sha256').update(String(b)).digest();
|
|
return ha.length === hb.length && crypto.timingSafeEqual(ha, hb);
|
|
}
|
|
|
|
app.use(express.json({ limit: '256kb' }));
|
|
app.use('/static', express.static(path.join(__dirname, 'public')));
|
|
|
|
app.get('/api/data', (req, res) => {
|
|
res.json(DB);
|
|
});
|
|
|
|
app.post('/api/data', (req, res) => {
|
|
if (!mustAuth(req)) return res.status(401).json({ error: 'unauthorized' });
|
|
const body = req.body || {};
|
|
const next = {};
|
|
|
|
if (body.profile) next.profile = sanitizeProfile(body.profile);
|
|
if (body.timezone) next.timezone = sanitizeTimezone(body.timezone);
|
|
if (body.now !== undefined) next.now = sanitizeNow(body.now);
|
|
if (body.posts !== undefined) next.posts = sanitizePosts(body.posts);
|
|
|
|
if (Object.keys(next).length === 0) return res.status(400).json({ error: 'nothing to update' });
|
|
|
|
DB = { ...DB, ...next };
|
|
saveData(DB);
|
|
res.json({ ok: true, data: DB });
|
|
});
|
|
|
|
function sanitizeProfile(p) {
|
|
return {
|
|
name: str(p.name, ''),
|
|
handle: str(p.handle, ''),
|
|
meta: str(p.meta, ''),
|
|
bio: str(p.bio, ''),
|
|
avatar: str(p.avatar, ''),
|
|
website: str(p.website, '')
|
|
};
|
|
}
|
|
function sanitizeTimezone(t) {
|
|
return { id: str(t.id, 'America/Chicago'), label: str(t.label, '') };
|
|
}
|
|
function sanitizeNow(n) {
|
|
if (!Array.isArray(n)) return [];
|
|
return n
|
|
.filter((x) => Array.isArray(x) && x.length >= 2)
|
|
.map((x) => [str(x[0], ''),
|
|
str(x[1], '')])
|
|
.filter((x) => x[0] || x[1]);
|
|
}
|
|
function sanitizePosts(ps) {
|
|
if (!Array.isArray(ps)) return [];
|
|
return ps
|
|
.filter((x) => x && typeof x === 'object')
|
|
.map((x) => ({
|
|
title: str(x.title, ''),
|
|
body: str(x.body, ''),
|
|
date: str(x.date, ''),
|
|
link: str(x.link, '')
|
|
}))
|
|
.filter((x) => x.title);
|
|
}
|
|
function str(v, dflt) {
|
|
return typeof v === 'string' ? v.slice(0, 2000) : dflt;
|
|
}
|
|
|
|
app.get('/admin', (req, res) => {
|
|
const html = fs.readFileSync(path.join(__dirname, 'views', 'admin.html'), 'utf8');
|
|
res.set('Content-Type', 'text/html');
|
|
res.send(html.replace('{{DATA}}', JSON.stringify(DB)));
|
|
});
|
|
|
|
app.get('/', (req, res) => {
|
|
const html = fs.readFileSync(path.join(__dirname, 'views', 'share.html'), 'utf8');
|
|
res.set('Content-Type', 'text/html');
|
|
res.send(html.replace('{{DATA}}', JSON.stringify(DB)));
|
|
});
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`share server running on http://localhost:${PORT}`);
|
|
console.log(`admin page: http://localhost:${PORT}/admin`);
|
|
console.log(`api: http://localhost:${PORT}/api/data`);
|
|
});
|