'use strict'; /** * Start des API-Dienstes `aircard-api` (Node 22, nur Bordmittel): liest die Umgebung, baut createApp und * lauscht auf einem Port. Beschreibung der Endpunkte: app.js und README.md. * * Umgebung: * AIRCARD_API_KEY Pflicht Appwrite-Server-Key `aircard-api` (Console-Scopes documents.read/write = rows.* der API, files.read/write) * AIRCARD_API_SALT Pflicht Salz für die Melder-Hashes (32 Byte hex) * AIRCARD_APPWRITE_PROJECT_ID Pflicht Projekt-ID * AIRCARD_APPWRITE_ENDPOINT http://webklar-appwrite/v1 * AIRCARD_API_PORT 8088 * Fehlt etwas Pflichtiges, nennt die Meldung nur den Namen der Variable (nie Werte) und der Prozess endet mit Exit-Code 1. */ const http = require('node:http'); const { createAppwrite } = require('./appwrite.js'); const { createApp } = require('./app.js'); const DEFAULT_PORT = 8088; const DEFAULT_ENDPOINT = 'http://webklar-appwrite/v1'; /** * Prüft die Umgebung, ohne etwas zu starten. * @returns {{ok: true, config: object, warnings: string[]} | {ok: false, error: string}} */ function loadConfig(env) { const read = (name) => (typeof env[name] === 'string' ? env[name].trim() : ''); const missing = ['AIRCARD_API_KEY', 'AIRCARD_API_SALT', 'AIRCARD_APPWRITE_PROJECT_ID'].filter((name) => read(name) === ''); if (missing.length > 0) { return { ok: false, error: `${missing.join(', ')} ${missing.length > 1 ? 'sind' : 'ist'} nicht gesetzt - Abbruch` }; } const endpoint = read('AIRCARD_APPWRITE_ENDPOINT') || DEFAULT_ENDPOINT; if (!/^https?:\/\/\S+$/.test(endpoint)) { return { ok: false, error: 'AIRCARD_APPWRITE_ENDPOINT muss mit http:// oder https:// beginnen - Abbruch' }; } const portText = read('AIRCARD_API_PORT'); const port = portText === '' ? DEFAULT_PORT : Number(portText); if (!Number.isInteger(port) || port < 0 || port > 65535) { return { ok: false, error: 'AIRCARD_API_PORT ist keine gültige Portnummer - Abbruch' }; } const warnings = []; if (read('AIRCARD_API_SALT').length < 16) warnings.push('AIRCARD_API_SALT ist sehr kurz (empfohlen: 32 Byte hex)'); return { ok: true, warnings, config: { port, endpoint, project: read('AIRCARD_APPWRITE_PROJECT_ID'), key: read('AIRCARD_API_KEY'), salt: read('AIRCARD_API_SALT'), }, }; } function main() { const say = (message) => console.log(`[aircard-api] ${new Date().toISOString()} ${message}`); const loaded = loadConfig(process.env); if (!loaded.ok) { console.error(`[aircard-api] ${loaded.error}`); process.exit(1); } for (const warning of loaded.warnings) say(`Warnung: ${warning}`); const { port, endpoint, project, key, salt } = loaded.config; const appwrite = createAppwrite({ endpoint, project, key }); const app = createApp({ appwrite, salt, log: console.log }); const server = http.createServer(app); // Langsame oder hängende Clients nicht ewig halten; Keep-Alive länger als Traefiks Leerlauf (90 s), // damit Traefik keine Verbindung wiederverwendet, die Node gerade schließt. server.headersTimeout = 10_000; server.requestTimeout = 15_000; server.keepAliveTimeout = 95_000; server.listen(port, () => say(`listening on :${server.address().port} (Appwrite: ${endpoint})`)); const shutdown = (signal) => { say(`${signal} received, shutting down`); server.close(() => process.exit(0)); server.closeIdleConnections(); setTimeout(() => process.exit(0), 5000).unref(); }; process.once('SIGTERM', () => shutdown('SIGTERM')); process.once('SIGINT', () => shutdown('SIGINT')); } module.exports = { loadConfig }; if (require.main === module) { main(); }