293 lines
11 KiB
JavaScript
293 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;
|
||
|
|
}
|