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>
Deployment aircard-ios.org
Die Seite wird auf dem Webklar-Server aus dem Gitea-Repo knso/AirCardIOS gebaut und von nginx ausgeliefert. Traefik stellt TLS und Weiterleitungen. Dieser Ordner enthält alles, was dafür im Repo liegt; die Dateien auf dem Server sind Kopien davon.
Bestandteile
| Datei im Repo | Ziel auf dem Server (/home/webklar/apps/) |
Aufgabe |
|---|---|---|
deploy/deploy.sh |
aircard-ios.org/deploy.sh (ausführbar) |
klont/aktualisiert das Repo, baut web/ und kopiert dist/ nach out/ |
deploy/webhook-server.js |
aircard-ios.org/webhook-server.js |
nimmt den Gitea-Webhook an, startet deploy.sh nacheinander, pollt alle 15 Minuten |
deploy/nginx/aircard-ios.org.conf |
aircard-ios.org/nginx/aircard-ios.org.conf |
nginx-Konfiguration der Seite |
deploy/traefik/aircard.yml |
traefik/dynamic/aircard.yml |
Route /v1 mit Header X-Appwrite-Project: 6ac2130a00289343d6d5 an Appwrite |
deploy/api/appwrite.js, app.js, server.js |
aircard-ios.org/api/ (nur diese drei Dateien) |
API-Dienst aircard-api der Community-Galerie, siehe api/README.md |
deploy/compose-snippet.yml |
docker-compose.yml (nach dem eship-Block) |
Dienste aircard-build, aircard und aircard-api |
Weitere Ordner unter aircard-ios.org/ (nicht im Repo): build/ (Klon des Repos, Arbeitsordner des Build-Containers), out/ (gebaute Seite, von nginx ausgeliefert), dev/ (Arbeitskopie für die Entwicklung, nicht gemountet).
Einrichten (einmalig)
APP=/home/webklar/apps
SRC=$APP/aircard-ios.org/dev/deploy
mkdir -p $APP/aircard-ios.org/build $APP/aircard-ios.org/out $APP/aircard-ios.org/nginx $APP/aircard-ios.org/api
cp $SRC/deploy.sh $APP/aircard-ios.org/deploy.sh && chmod +x $APP/aircard-ios.org/deploy.sh
cp $SRC/webhook-server.js $APP/aircard-ios.org/webhook-server.js
cp $SRC/nginx/aircard-ios.org.conf $APP/aircard-ios.org/nginx/aircard-ios.org.conf
cp $SRC/api/appwrite.js $SRC/api/app.js $SRC/api/server.js $APP/aircard-ios.org/api/
# Compose: Backup, dann den Inhalt von compose-snippet.yml unter `services:` direkt nach dem eship-Block einfügen
cp $APP/docker-compose.yml $APP/docker-compose.yml.bak-2026-10-04-aircard
cd $APP && docker compose config -q; echo $? # 0; einzige erlaubte Ausgabe: Warnung "attribute `version` is obsolete"
# Traefik-Route atomar ablegen (Datei-Provider lädt sie ohne Neustart)
cp $SRC/traefik/aircard.yml $APP/traefik/dynamic/aircard.yml.tmp && mv $APP/traefik/dynamic/aircard.yml.tmp $APP/traefik/dynamic/aircard.yml
Die .env neben der docker-compose.yml muss GITEA_DEPLOY_TOKEN (Lesezugriff auf knso/AirCardIOS) und GITEA_DEPLOY_WEBHOOK_SECRET enthalten, für den API-Dienst außerdem AIRCARD_APPWRITE_PROJECT_ID, AIRCARD_API_KEY und AIRCARD_API_SALT. Diese Werte gehören nie in Logs, Chats oder Commits. Das Salz entsteht, ohne dass der Wert auf dem Bildschirm erscheint:
printf 'AIRCARD_API_SALT=%s\n' "$(openssl rand -hex 32)" >> /home/webklar/apps/.env
Den Key AIRCARD_API_KEY legt Kenso nach docs/appwrite-console.md (Abschnitt 7.3) an und trägt ihn mit dem verdeckten read -s-Befehl ein.
Starten
cd /home/webklar/apps && docker compose up -d aircard-build aircard aircard-api
Nur diese drei Dienste starten, nie ein nacktes docker compose up -d (das würde nicht laufende Dienste wie eship-build wieder anlegen). Der Build-Container installiert beim Start git, startet den Webhook-Server und baut sofort einmal (initial). Den Fortschritt zeigt docker logs -f webklar-aircard-build; am Ende steht Deployment abgeschlossen (oder eine Zeile mit ERROR:).
Ohne AIRCARD_WEBHOOK_TOKEN (also ohne GITEA_DEPLOY_WEBHOOK_SECRET in der .env) beendet sich der Webhook-Server mit Exit-Code 1, statt Anfragen ungeprüft durchzulassen. Ebenso beendet sich aircard-api mit Exit-Code 1, wenn AIRCARD_API_KEY, AIRCARD_API_SALT oder AIRCARD_APPWRITE_PROJECT_ID fehlen (die Meldung nennt nur den Namen der Variable).
API-Dienst aircard-api
Kleiner Node-Dienst ohne Abhängigkeiten (deploy/api/), Container webklar-aircard-api. Er führt alle Schreibzugriffe der Community-Galerie mit dem Appwrite-Server-Key aus (Veröffentlichen, Zurückziehen, Melden mit Auto-Ausblenden ab 3 Meldern, Admin-Sichtbarkeit). Endpunkte, Statuscodes und Betrieb stehen in api/README.md.
- Route: Traefik-Label am Dienst, Router
aircard-gallery-api(Host(`aircard-ios.org`) && PathPrefix(`/api`), Priorität 200, Port 8088). Der Name ist absichtlich nichtaircard-api: so heißt schon die Appwrite-Route (/v1) intraefik/dynamic/aircard.yml. - Prüfen nach dem Start:
curl -s https://aircard-ios.org/api/gesundheitliefert{"ok":true};docker logs webklar-aircard-apizeigtlistening on :8088. - Ändern: Dateien mit
cpnachaircard-ios.org/api/ersetzen, danncd /home/webklar/apps && docker compose restart aircard-api. - Kein Port nach außen: der Dienst vertraut dem Header
X-Real-Ip(setzt Traefik) für Rate-Limit und Melder-Hash, deshalb nieports:ergänzen.
Manuell deployen
docker exec webklar-aircard-build sh /deploy.sh
Ohne DEPLOY_TRIGGER baut das Skript immer neu, auch wenn sich der Commit nicht geändert hat.
Es läuft immer nur ein Deployment: Ist schon eines aktiv (Sperrverzeichnis build/.git/aircard-deploy.lock), schreibt das Skript „Deployment läuft bereits“ und endet mit Exit-Code 0. Eine verwaiste Sperre (Prozess weg oder älter als 60 Minuten) übernimmt der nächste Lauf selbst. npm ci und der Build brechen nach 15 Minuten ab (timeout 900).
Gitea-Webhook
Repo knso/AirCardIOS → Einstellungen → Webhooks → Gitea:
| Feld | Wert |
|---|---|
| Ziel-URL | https://aircard-ios.org/webhook/deploy |
| HTTP-Methode | POST |
| Content-Type | application/json |
| Secret | Wert von GITEA_DEPLOY_WEBHOOK_SECRET aus /home/webklar/apps/.env (volle Länge, nur dort nachsehen) |
| Auslöser | nur Push-Ereignisse |
| Branch-Filter | main |
| Aktiv | ja |
„Test delivery“ muss 202 ergeben. 401 heißt: Secret stimmt nicht. 200 mit {"ignored":true} heißt: Push auf einen anderen Branch als main.
Verhalten
- Webhook (
POST /webhook/deploy): prüft die Signatur im Headerx-gitea-signature(HMAC-SHA256 über den Rohbody).401bei falscher oder fehlender Signatur,413bei Body über 1 MB,200bei Push auf einen anderen Branch,202bei Push aufmain; der Build läuft danach im Hintergrund. Alles andere antwortet404. - Warteschlange: Builds laufen nacheinander, höchstens einer läuft und einer wartet. Weitere Pushes während ein Lauf wartet, werden mit
202beantwortet und fügen keinen Lauf hinzu (der wartende Lauf holt ohnehin den neuesten Stand). - Trigger:
webhookund manuelle Aufrufe bauen immer.initial(Container-Start) undpoll(alle 15 Minuten) bauen nur, wenn es einen neuen Commit gibt oderout/index.htmlfehlt. - Gemerkter Commit:
build/.git/aircard-last-deployed-commitenthält den zuletzt versuchten Commit, geschrieben vor dem Build. Ein kaputter Build wird deshalb nicht alle 15 Minuten wiederholt; die alte Seite bleibt online, bis ein neuer Commit oder ein Webhook-/manueller Lauf kommt. - Fehler: Ein fehlgeschlagener Lauf wird mit
[webhook] … deployment failedprotokolliert und beendet den Server nicht. Fehlerzeilen des Skripts beginnen mitERROR:. - Build-Variablen:
VITE_APPWRITE_ENDPOINTundVITE_APPWRITE_PROJECT_IDkommen aus der Container-Umgebung (compose);AIRCARD_APPWRITE_PROJECT_IDwird aus/home/webklar/apps/.envgelesen. Vite liest sie direkt. - .env-Variablen:
AIRCARD_APPWRITE_PROJECT_ID(Projekt-ID6ac2130a00289343d6d5) undAIRCARD_APPWRITE_API_KEY(für das Setup-Skript) müssen in/home/webklar/apps/.enveingetragen sein; füraircard-apizusätzlichAIRCARD_API_KEY(Keyaircard-api) undAIRCARD_API_SALT.
Änderungen später
deploy.shändern: Datei auf dem Server mitcpüberschreiben (nichtmv, sonst zeigt der Datei-Mount auf die alte Datei). Gilt beim nächsten Lauf.webhook-server.jsändern: ebenso mitcpersetzen, danncd /home/webklar/apps && docker compose restart aircard-build.deploy/api/*.jsändern: ebenso mitcpersetzen, danncd /home/webklar/apps && docker compose restart aircard-api.- nginx-Konfiguration ändern: mit
cpersetzen, danndocker exec webklar-aircard nginx -t && docker exec webklar-aircard nginx -s reload. - Traefik-Route ändern: Datei atomar ersetzen wie oben; Löschen der Datei entfernt die Route wieder.
www.aircard-ios.org
Solange dig +short www.aircard-ios.org @8.8.8.8 nichts liefert, gibt es nur die Hauptdomain. Zeigt der A-Record auf 91.99.156.85, werden die auskommentierten www-Labels am Dienst aircard in docker-compose.yml aktiviert (Backup vorher, dann docker compose config -q und docker compose up -d aircard). Sie leiten per 301 auf https://aircard-ios.org weiter und haben eigene Router, damit ein www-Fehler das Zertifikat der Hauptdomain nicht blockiert.
Tests
node --test deploy/webhook-server.test.js deploy/deploy.test.js
node --test "deploy/api/*.test.js"
Die Tests brauchen nur Node und starten die Server auf Port 0. Die API-Tests laufen gegen einen Fake-Appwrite im Speicher (deploy/api/fake-appwrite.js); das echte Appwrite wird nie angesprochen. Das Glob muss in Anführungszeichen stehen.