kua-money-trace/src/server.js

116 lines
4.0 KiB
JavaScript

#!/usr/bin/env node
import http from 'node:http';
import fs from 'node:fs/promises';
import path from 'node:path';
import { URL } from 'node:url';
import { buildMoneyGraph, destinationTree, originTree, summarizeLedger } from './moneyGraph.js';
import { loadLedger, resolveAccountOwnerHints } from './ledgerStore.js';
const args = parseArgs(process.argv.slice(2));
const ledgerPath = args.ledger || 'data/example-ledger.json';
const port = Number(args.port || 3910);
const host = args.host || '127.0.0.1';
const webDir = path.resolve(args.web || 'web');
let ledger = resolveAccountOwnerHints(await loadLedger(ledgerPath));
let graph = buildMoneyGraph(ledger);
const MIME = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.ico': 'image/x-icon',
};
const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url, `http://${req.headers.host}`);
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
if (req.method === 'GET' && url.pathname === '/health') {
return sendJson(res, { status: 'ok', service: 'kua-money-trace', ledger: ledgerPath });
}
if (req.method === 'POST' && url.pathname === '/reload') {
ledger = resolveAccountOwnerHints(await loadLedger(ledgerPath));
graph = buildMoneyGraph(ledger);
return sendJson(res, { reloaded: true, summary: summarizeLedger(ledger, graph) });
}
if (req.method === 'GET' && url.pathname === '/summary') {
return sendJson(res, summarizeLedger(ledger, graph));
}
if (req.method === 'GET' && url.pathname === '/entities') {
return sendJson(res, { entities: ledger.entities });
}
if (req.method === 'GET' && url.pathname === '/accounts') {
return sendJson(res, { accounts: ledger.accounts });
}
if (req.method === 'GET' && url.pathname === '/movements') {
return sendJson(res, { movements: ledger.movements });
}
const originMatch = url.pathname.match(/^\/nodes\/(.+)\/origin-tree$/);
if (req.method === 'GET' && originMatch) {
return sendJson(res, originTree(graph, decodeURIComponent(originMatch[1])));
}
const destinationMatch = url.pathname.match(/^\/nodes\/(.+)\/destination-tree$/);
if (req.method === 'GET' && destinationMatch) {
return sendJson(res, destinationTree(graph, decodeURIComponent(destinationMatch[1])));
}
// static files from web/
if (req.method === 'GET') {
const filePath = path.join(webDir, url.pathname === '/' ? 'dashboard.html' : url.pathname);
const realPath = path.resolve(filePath);
if (!realPath.startsWith(webDir + path.sep) && realPath !== webDir) {
return sendJson(res, { error: 'forbidden' }, 403);
}
try {
const data = await fs.readFile(realPath);
const ext = path.extname(realPath);
res.writeHead(200, { 'content-type': MIME[ext] || 'application/octet-stream' });
res.end(data);
return;
} catch {
// fall through to 404
}
}
sendJson(res, { error: 'not found' }, 404);
} catch (error) {
sendJson(res, { error: error.message }, 500);
}
});
server.listen(port, host, () => {
console.log(`kua-money-trace listening on http://${host}:${port}`);
console.log(` dashboard: http://${host}:${port}/dashboard.html`);
console.log(` ledger: ${ledgerPath}`);
});
function sendJson(res, body, status = 200) {
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(body, null, 2));
}
function parseArgs(argv) {
const parsed = {};
for (let i = 0; i < argv.length; i += 1) {
if (argv[i].startsWith('--')) {
parsed[argv[i].slice(2)] = argv[i + 1];
i += 1;
}
}
return parsed;
}