Files
AtreidesAssetsFree/server/api.mjs
T
2026-10-29 11:57:29 +01:00

294 lines
11 KiB
JavaScript

// Generated, do not edit. sha256:6b0db04ca1e019c207aa13f2f0133b94c5f5783d9d7742ec8001cff39b53d404
import * as custom from './price/custom.js';
import { getKeys, setKeys, keyStatus } from './keys.mjs';
import * as market from './market-service.mjs';
import { handleDataApi } from './data-api.mjs';
import { recordIntegrationCall } from './data/integration-log.js';
import { mcpClientConfig } from './mcp/client-config.mjs';
import { dryRunTool, dryRunTools } from './mcp/dry-run.mjs';
import { exportDatabase } from './db-export.mjs';
import { exportPriceHistory } from './price-export.mjs';
import { exportIntegrations } from './integrations-export.mjs';
import { restoreDatabase } from './db-restore.mjs';
import { startFreshDatabase, chooseSetupCurrency } from './db-fresh.mjs';
import { createBackup, restoreBackup, isBackupBestand } from './db-backup.mjs';
import {
overviewPortfolios, createPortfolio, renamePortfolio, archivePortfolio, purgeArchived,
activatePortfolio,
} from './portfolios-admin.mjs';
import { credentialsStatus, setCredentials, clearCredentials } from './auth.mjs';
import { closeDatabase } from './db.js';
import { apiRoutes, services } from './registers.mjs';
import { CONFIG } from '../shared/config.js';
import { AppError, errorPayload } from '../shared/app-error.js';
function stopVoorWissel() {
try { closeDatabase(); } catch { }
process.exit(CONFIG.PORTFOLIO.RESTART_EXIT_CODE);
}
function stopVoorSluiten() {
services.get('kiosk')?.close();
try { closeDatabase(); } catch { }
process.exit(0);
}
const isLoopback = (req) => ['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(req.socket?.remoteAddress);
const recordedBrowser = () => services.get('kiosk')?.recorded() ?? null;
function sendJson(res, status, obj) {
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'X-Content-Type-Options': 'nosniff' });
res.end(JSON.stringify(obj));
}
const sendFout = (res, status, e) => sendJson(res, status, errorPayload(e));
function readRawBody(req, max = 100 * 1024 * 1024) {
return new Promise((resolve, reject) => {
let len = 0; const chunks = [];
req.on('data', c => {
len += c.length;
if (len > max) { req.destroy(); reject(new AppError('common.fileTooLarge', null, 'File too large.')); }
else chunks.push(c);
});
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
}
export function readJsonBody(req, max = 256 * 1024) {
return new Promise((resolve, reject) => {
let len = 0; const chunks = [];
req.on('data', c => {
len += c.length;
if (len > max) { req.destroy(); reject(new Error('Body te groot.')); }
else chunks.push(c);
});
req.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf-8');
if (!raw) return resolve({});
try { resolve(JSON.parse(raw)); } catch { reject(new Error('Invalid JSON.')); }
});
req.on('error', reject);
});
}
export async function handleApi(req, res, url) {
const u = new URL(url, 'http://localhost');
const path = u.pathname;
const q = u.searchParams;
if (path === '/api/health') { sendJson(res, 200, { ok: true }); return true; }
if (path === '/api/app/window' && req.method === 'GET') {
sendJson(res, 200, { canClose: isLoopback(req) && recordedBrowser() !== null });
return true;
}
if (path === '/api/app/close' && req.method === 'POST') {
if (!isLoopback(req) || !recordedBrowser()) {
sendFout(res, 409, new AppError('appWindow.closeUnavailable', null,
'Closing is only possible on this machine, in the app window the app started.'));
return true;
}
res.on('finish', stopVoorSluiten);
sendJson(res, 200, { closing: true });
return true;
}
if (path.startsWith('/api/data/')) return handleDataApi(req, res, url);
if (path === '/api/mcp/config' && req.method === 'GET') {
sendJson(res, 200, mcpClientConfig());
return true;
}
if (path === '/api/mcp/tools' && req.method === 'GET') {
sendJson(res, 200, { tools: dryRunTools() });
return true;
}
if (path === '/api/mcp/dry-run' && req.method === 'POST') {
try {
const body = await readJsonBody(req);
sendJson(res, 200, await dryRunTool(body.tool, body.arguments));
} catch (e) {
sendFout(res, 400, e);
}
return true;
}
if (path === '/api/db/export' && req.method === 'GET') {
try { await exportDatabase(res); }
catch (e) { sendFout(res, 500, e); }
return true;
}
if (path === '/api/price-history/export' && req.method === 'GET') {
try { exportPriceHistory(res); }
catch (e) { sendFout(res, 500, e); }
return true;
}
if (path === '/api/integrations/export' && req.method === 'GET') {
try { exportIntegrations(res); }
catch (e) { sendFout(res, 500, e); }
return true;
}
if (path === '/api/db/backup' && req.method === 'POST') {
try {
const { passphrase } = await readJsonBody(req);
const { bytes, db, secrets } = await createBackup(passphrase);
const d = new Date();
const p = n => String(n).padStart(2, '0');
const naam = `assetapp-${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}.assetbak`;
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename="${naam}"`,
'Content-Length': bytes.length,
'X-Backup-Db-Bytes': String(db),
'X-Backup-Secrets-Bytes': String(secrets),
'Cache-Control': 'no-store',
'X-Content-Type-Options': 'nosniff',
});
res.end(bytes);
} catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/db/import' && req.method === 'POST') {
try {
const bytes = await readRawBody(req);
const uit = isBackupBestand(bytes)
? await restoreBackup(bytes, req.headers['x-backup-passphrase'] || '')
: await restoreDatabase(bytes);
sendJson(res, 200, { ...uit, versleuteld: isBackupBestand(bytes) });
} catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/db/fresh' && req.method === 'POST') {
try { sendJson(res, 200, await startFreshDatabase()); }
catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/setup/currency' && req.method === 'POST') {
try { sendJson(res, 200, await chooseSetupCurrency((await readJsonBody(req)).baseCurrency)); }
catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/portfolios' && req.method === 'GET') {
try { sendJson(res, 200, await overviewPortfolios()); }
catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/portfolios/activate' && req.method === 'POST') {
try {
const uit = activatePortfolio((await readJsonBody(req)).naam);
if (uit.gewisseld) res.on('finish', stopVoorWissel);
sendJson(res, 200, uit);
} catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/portfolios/create' && req.method === 'POST') {
try {
const { naam, kopieVan, baseCurrency } = await readJsonBody(req);
sendJson(res, 200, await createPortfolio(naam, { kopieVan: kopieVan || null, baseCurrency: baseCurrency || null }));
} catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/portfolios/rename' && req.method === 'POST') {
try {
const { van, naar } = await readJsonBody(req);
sendJson(res, 200, await renamePortfolio(van, naar));
} catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/portfolios/archive' && req.method === 'POST') {
try { sendJson(res, 200, await archivePortfolio((await readJsonBody(req)).naam)); }
catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/portfolios/purge' && req.method === 'POST') {
try { sendJson(res, 200, await purgeArchived((await readJsonBody(req)).naam)); }
catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/auth/status' && req.method === 'GET') {
try { sendJson(res, 200, await credentialsStatus()); }
catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/auth/set' && req.method === 'POST') {
try {
const { password } = await readJsonBody(req);
sendJson(res, 200, await setCredentials(password));
} catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/auth/clear' && req.method === 'POST') {
try { sendJson(res, 200, await clearCredentials()); }
catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/price/call' && req.method === 'POST') {
let provider = null, capability = null, symbol = null, creds = null;
const t0 = performance.now();
try {
const body = await readJsonBody(req);
({ provider, capability, symbol } = body);
const { date, with_raw } = body;
if (!provider || !provider.key) throw new Error('The provider config is missing.');
creds = await getKeys(provider.key);
sendJson(res, 200, await custom.priceCall(provider, capability, symbol, date, creds, { withRaw: !!with_raw }));
} catch (e) {
if (provider?.key) {
try {
await recordIntegrationCall(custom.failureEntry(provider, capability, symbol, e, performance.now() - t0),
{ secrets: Object.values(creds || {}).filter(v => typeof v === 'string') });
} catch { }
}
sendFout(res, 502, e);
}
return true;
}
if (path === '/api/keys/status' && req.method === 'GET') {
try { sendJson(res, 200, await keyStatus(q.get('provider'), q.get('authenticationKind'))); }
catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/keys/set' && req.method === 'POST') {
try {
const { provider, authenticationKind, values } = await readJsonBody(req);
sendJson(res, 200, await setKeys(provider, authenticationKind, values));
} catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/market/status' && req.method === 'GET') {
try { sendJson(res, 200, await market.marketServiceStatus()); }
catch (e) { sendFout(res, 400, e); }
return true;
}
if (path === '/api/market/test' && req.method === 'POST') {
try { sendJson(res, 200, await market.testMarketService()); }
catch (e) { sendFout(res, 502, e); }
return true;
}
const route = apiRoutes.list().find(r => r.path === path && r.method === req.method);
if (route) {
try {
const body = req.method === 'POST' ? await readJsonBody(req) : {};
sendJson(res, 200, await route.run({ q, body }));
} catch (e) { sendFout(res, route.errorStatus ?? 400, e); }
return true;
}
return false;
}