Files
ClaudeandClaude Sonnet 5.5 f6b9917b07 docs: Scope-Namen der Console (documents.*), sicherer .env-Befehl, X-Real-Ip-Risiko
- Der Key aircard-api braucht in der Console documents.read/documents.write (entspricht rows.* der API) sowie files.read/files.write.
- Der Befehl zum Eintragen des Keys nutzt read -rs, schreibt bei leerer Eingabe nichts und beginnt mit einem Zeilenumbruch
  (klebt nie an einer letzten .env-Zeile ohne Umbruch); Hinweis zum Zeilenumbruch auch bei der Eingabe mit nano.
- deploy/README.md: nginx-503 bei fehlendem Dienst, up -d nach .env-Änderung, X-Real-Ip im Docker-Netz als akzeptiertes Risiko.
- Kommentare in server.js und compose-snippet.yml angepasst.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 16:40:17 +00:00

100 lines
3.6 KiB
JavaScript

'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();
}