Files
AtreidesAssetsFree/server/app.mjs
T

167 lines
6.3 KiB
JavaScript
Raw Normal View History

2026-10-29 11:57:29 +01:00
// Generated, do not edit. sha256:29c149da1bd119d466012f8c80b13c9f6ba6ac924e6326dd2b0629b05ed6ce6e
import { readFile } from 'node:fs/promises';
import { join, resolve, normalize, extname, dirname, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { handleApi, readJsonBody } from './api.mjs';
import {
clearedSessionCookie, passwordMatches, sendUnauthorized, sessionCookie, sessionTokenOf,
} from './auth.mjs';
import { createAttemptLimiter, createSessionStore } from './sessions.mjs';
import { CONFIG } from '../shared/config.js';
import { connectSrcOrigins } from '../shared/csp.js';
import { listConnections } from './data/connections.js';
import { errorPayload } from '../shared/app-error.js';
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
export const CLIENT = join(ROOT, 'client');
const SHARED = join(ROOT, 'shared');
const MIME = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.mjs': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.wasm': 'application/wasm',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.webp': 'image/webp',
'.ico': 'image/x-icon',
'.woff2': 'font/woff2',
'.woff': 'font/woff',
'.ttf': 'font/ttf',
};
function explorerUrls() {
try { return listConnections('btc_explorer').map(c => c.url); } catch { return []; }
}
const directive = (naam, ...bronnen) => [naam, ...bronnen.flat().filter(Boolean)].join(' ');
const csp = () => [
directive('default-src', `'self'`),
directive('script-src', `'self'`),
directive('style-src', `'self'`, `'unsafe-inline'`),
directive('style-src-elem', `'self'`),
directive('style-src-attr', `'unsafe-inline'`),
directive('font-src', `'self'`),
directive('img-src', `'self'`, 'data:'),
directive('connect-src', `'self'`, connectSrcOrigins(explorerUrls())),
directive('object-src', `'none'`),
directive('base-uri', `'self'`),
directive('frame-ancestors', `'none'`),
].join('; ');
const HTML_CACHE_CONTROL = 'no-store';
const STATIC_CACHE_CONTROL = 'no-cache';
function resolveStatic(urlPath) {
let rel;
try { rel = decodeURIComponent(urlPath.split('?')[0]); }
catch { return null; }
if (rel === '/' || rel === '/index.html') return join(CLIENT, 'index.html');
if (rel === CONFIG.AUTH.LOGIN_PATH) return join(CLIENT, 'login.html');
const onder = (basis, pad) => {
const abs = normalize(join(basis, pad));
return abs.startsWith(basis + sep) ? abs : null;
};
if (rel.startsWith('/shared/')) return onder(SHARED, rel.slice('/shared'.length));
return onder(CLIENT, rel);
}
function send(res, status, body, headers = {}) {
res.writeHead(status, { 'X-Content-Type-Options': 'nosniff', ...headers });
res.end(body);
}
function sendJson(res, status, obj, extra = {}) {
send(res, status, JSON.stringify(obj), { 'Content-Type': 'application/json; charset=utf-8', ...extra });
}
const LOGIN_ASSETS = new Set([
'/login.js',
'/css/styles.css',
'/img/favicon.svg',
'/vendor/inter/inter.css',
]);
const LOGIN_ASSET_PREFIX = '/vendor/inter/files/';
const magZonderSessie = (pad) =>
pad === CONFIG.AUTH.LOGIN_PATH || LOGIN_ASSETS.has(pad) || pad.startsWith(LOGIN_ASSET_PREFIX);
export function createRequestListener({ credentials = null, secure = false } = {}) {
const sessies = createSessionStore();
const limiet = createAttemptLimiter();
async function inloggen(req, res) {
const adres = req.socket?.remoteAddress || 'unknown';
const wacht = limiet.retryAfterMs(adres);
if (wacht > 0) {
return sendJson(res, 429, { error: 'Too many attempts.', retryAfterMs: wacht },
{ 'Retry-After': String(Math.ceil(wacht / 1000)) });
}
let body;
try { body = await readJsonBody(req); }
catch (e) { return sendJson(res, 400, errorPayload(e)); }
if (!passwordMatches(body?.password, credentials)) {
return sendJson(res, 401, { error: 'Wrong password.', retryAfterMs: limiet.fail(adres) });
}
limiet.succeed(adres);
return sendJson(res, 200, { ok: true }, { 'Set-Cookie': sessionCookie(sessies.create(), { secure }) });
}
function uitloggen(req, res) {
const token = sessionTokenOf(req);
if (token) sessies.drop(token);
return sendJson(res, 200, { ok: true }, { 'Set-Cookie': clearedSessionCookie({ secure }) });
}
return async (req, res) => {
try {
const url = req.url || '/';
const pad = url.split('?')[0];
if (credentials) {
if (pad === '/api/login') {
if (req.method !== 'POST') return sendJson(res, 405, { error: 'Method not allowed' });
return await inloggen(req, res);
}
if (pad === '/api/logout') {
if (req.method !== 'POST') return sendJson(res, 405, { error: 'Method not allowed' });
return uitloggen(req, res);
}
const ingelogd = sessies.isValid(sessionTokenOf(req));
if (ingelogd && pad === CONFIG.AUTH.LOGIN_PATH) {
return send(res, 302, '', { Location: '/', 'Cache-Control': 'no-store' });
}
if (!ingelogd && !magZonderSessie(pad)) {
return sendUnauthorized(res, { document: !pad.startsWith('/api/') });
}
} else if (pad === CONFIG.AUTH.LOGIN_PATH) {
return send(res, 302, '', { Location: '/', 'Cache-Control': 'no-store' });
}
if (url.startsWith('/api/')) {
const handled = await handleApi(req, res, url);
if (!handled) sendJson(res, 404, { error: 'Unknown endpoint' });
return;
}
if (req.method !== 'GET' && req.method !== 'HEAD') {
return sendJson(res, 405, { error: 'Method not allowed' });
}
const file = resolveStatic(url);
if (!file) return send(res, 404, 'Not found');
const data = await readFile(file);
const ext = extname(file).toLowerCase();
const type = MIME[ext] || 'application/octet-stream';
const cacheControl = ext === '.html' ? HTML_CACHE_CONTROL : STATIC_CACHE_CONTROL;
return send(res, 200, req.method === 'HEAD' ? '' : data, { 'Content-Type': type, 'Content-Security-Policy': csp(), 'Cache-Control': cacheControl });
} catch (err) {
if (err && err.code === 'ENOENT') return send(res, 404, 'Not found');
console.error('[assetapp] requestfout:', err);
return send(res, 500, 'Interne fout');
}
};
}