- 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>
100 lines
3.6 KiB
JavaScript
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();
|
|
}
|