feat(api): aircard-api für Veröffentlichen, Melden und Admin
Neuer abhängigkeitsfreier Node-22-Dienst in deploy/api/ (appwrite.js, app.js,
server.js), der alle Galerie-Schreibzugriffe mit dem Appwrite-Server-Key
ausführt: POST /api/veroeffentlichen, /zurueckziehen, /melden (Rate-Limit
10/h je Melder, IPv6 auf /64, Auto-Ausblenden ab 3), /admin/sichtbarkeit und
GET /api/gesundheit.
- JWT-Prüfung über GET /account (Header X-Aircard-Jwt), Admin = Label admin
- IDs per Muster geprüft, Pfadsegmente kodiert, "." und ".." abgelehnt
- deterministische Zeilen-IDs (Galerie, Meldung): Appwrite-409 ersetzt jede
Doppelt-Prüfung; Sperre je Karte gegen verlorene Zählerstände
- Dateiprüfung: Bucket, delete("user:<ich>"), PNG/WebP, höchstens 5 MiB
- Appwrite-Aufrufe mit 10-s-Zeitlimit, Logs ohne Key, Salz, JWT und IPs
- Tests gegen einen Fake-Appwrite im Speicher (70 Tests), Compose-Service
aircard-api (Router aircard-gallery-api) und README ergänzt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e4f4ffacc
commit
dd1597862a
11 files changed
+2659
-13
No files matched your search
@@ -0,0 +1,99 @@
|
||||
'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` (rows.read/write, 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();
|
||||
}
|
||||
Reference in new issue
Block a user