Erste Produktivversion

This commit is contained in:
2026-08-08 20:31:58 +02:00
parent d8ded2816d
commit 7b21e2d1b4
110 changed files with 18170 additions and 644 deletions

195
web/nginx.conf Normal file
View File

@@ -0,0 +1,195 @@
# Rate Limiting fuer Anmelde- und Registrierungsrouten.
# Zweite Verteidigungslinie - die Anwendung begrenzt zusaetzlich pro
# Konto, was nginx nicht kann.
limit_req_zone $binary_remote_addr zone=auth_zone:10m rate=10r/m;
limit_req_status 429;
server {
listen 8080;
server_name _;
server_tokens off;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
client_max_body_size 2m;
# Eingebauter DNS-Server von Docker. Ohne diesen loest nginx den
# Namen "api" nur EINMAL beim Start auf und merkt sich die IP
# dauerhaft - nach jedem Neustart des api-Containers gaebe es 502.
resolver 127.0.0.11 valid=10s ipv6=off;
resolver_timeout 5s;
# Steht in proxy_pass eine Variable, loest nginx pro Request neu auf.
# Dann muss die Ziel-URI aber explizit angehaengt werden - deshalb
# ueberall $request_uri (enthaelt auch den Query-String).
set $api_backend "http://api:8000";
# Sicherheitskopfzeilen. Siehe Kommentar in der Datei: sie muss in
# JEDER location wiederholt werden, die eigene add_header setzt.
include /etc/nginx/security_headers.conf;
# ------------------------------------------------------------------
# API
# ------------------------------------------------------------------
# Diese Pfade tragen einen Token im URI. Nicht protokollieren -
# siehe Begründung bei /s/.
location ~ ^/api/(public|invites)/ {
proxy_pass $api_backend$request_uri;
include /etc/nginx/proxy_common.conf;
access_log off;
}
location /api/auth/welcome {
proxy_pass $api_backend$request_uri;
include /etc/nginx/proxy_common.conf;
access_log off;
}
location /api/auth/email-change {
proxy_pass $api_backend$request_uri;
include /etc/nginx/proxy_common.conf;
access_log off;
}
location /api/auth/verify {
proxy_pass $api_backend$request_uri;
include /etc/nginx/proxy_common.conf;
access_log off;
}
location /api/ {
proxy_pass $api_backend$request_uri;
include /etc/nginx/proxy_common.conf;
}
# Ereigniskanal (Server-Sent Events).
location ~ ^/api/lists/[^/]+/events$ {
proxy_pass $api_backend$request_uri;
include /etc/nginx/proxy_common.conf;
# proxy_buffering, proxy_cache und die langen Zeitüberschreitungen
# stehen bereits in proxy_common.conf - hier nicht wiederholen,
# nginx lässt jede Direktive nur einmal je Kontext zu.
# Neu ist nur: keine Komprimierung, die würde die Ereignisse
# sammeln statt einzeln durchzureichen.
gzip off;
}
location /api/auth/ {
limit_req zone=auth_zone burst=20 nodelay;
proxy_pass $api_backend$request_uri;
include /etc/nginx/proxy_common.conf;
}
# Interaktive API-Dokumentation. Vor Produktivbetrieb auskommentieren
# oder per allow/deny auf das interne Netz beschraenken.
location ~ ^/(docs|redoc|openapi\.json)$ {
proxy_pass $api_backend$request_uri;
include /etc/nginx/proxy_common.conf;
}
location ~ ^/(healthz|readyz)$ {
access_log off;
proxy_pass $api_backend$request_uri;
include /etc/nginx/proxy_common.conf;
}
# Klartext statt HTML-Fehlerseite, damit ein fetch() im Browser eine
# verwertbare Meldung bekommt und nicht an JSON.parse scheitert.
error_page 502 503 504 = @backend_down;
location @backend_down {
default_type application/json;
return 503 '{"status":"error","detail":"Backend nicht erreichbar. Bitte docker compose logs api pruefen."}';
}
# ------------------------------------------------------------------
# Statische PWA
# ------------------------------------------------------------------
location = /sw.js {
include /etc/nginx/security_headers.conf;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
try_files $uri =404;
}
# Vom api-Container erzeugt, damit der Anwendungsname aus der .env
# auch auf dem Startbildschirm erscheint - ohne den web-Container
# bei jeder Umbenennung neu bauen zu müssen.
location = /manifest.webmanifest {
proxy_pass $api_backend$request_uri;
include /etc/nginx/proxy_common.conf;
}
# Von Vite gebaute Dateien tragen einen Hash im Namen - lange cachen.
location /assets/ {
include /etc/nginx/security_headers.conf;
add_header Cache-Control "public, max-age=31536000, immutable" always;
try_files $uri =404;
}
# Icons aendern sich praktisch nie.
location /icons/ {
include /etc/nginx/security_headers.conf;
add_header Cache-Control "public, max-age=604800" always;
try_files $uri =404;
}
# Öffentliche Listenlinks: nie zwischenspeichern, nie indexieren.
location /s/ {
include /etc/nginx/security_headers.conf;
add_header Cache-Control "no-store" always;
add_header X-Robots-Tag "noindex, nofollow, noarchive" always;
# Der Token IST die Berechtigung. Stünde er im Zugriffsprotokoll,
# käme jeder mit Leserecht auf die Logdateien an alle geteilten
# Listen - und Logdateien wandern in Sicherungen, werden an
# Auswertungswerkzeuge gereicht und leben länger als der Link.
access_log off;
try_files $uri /index.html;
}
location = /robots.txt {
include /etc/nginx/security_headers.conf;
default_type text/plain;
return 200 "User-agent: *\nDisallow: /s/\nDisallow: /invite\nDisallow: /reset\nDisallow: /willkommen\n";
}
# Aus demselben Grund wie bei /s/: Einladungs- und
# Passwort-Zurücksetzen-Links tragen ihren Token im Query-String,
# und den schreibt nginx sonst mit ins Protokoll.
location = /invite {
include /etc/nginx/security_headers.conf;
add_header Cache-Control "no-store" always;
add_header X-Robots-Tag "noindex, nofollow" always;
access_log off;
try_files $uri /index.html;
}
location = /willkommen {
include /etc/nginx/security_headers.conf;
add_header Cache-Control "no-store" always;
add_header X-Robots-Tag "noindex, nofollow" always;
access_log off;
try_files $uri /index.html;
}
location = /reset {
include /etc/nginx/security_headers.conf;
add_header Cache-Control "no-store" always;
add_header X-Robots-Tag "noindex, nofollow" always;
access_log off;
try_files $uri /index.html;
}
# Ohne Bundler tragen die JS-Dateien keinen Hash im Namen. "no-cache"
# heisst nicht "nicht speichern", sondern "vor Benutzung rueckfragen" -
# der Browser holt also nur bei Aenderung neu.
location / {
include /etc/nginx/security_headers.conf;
add_header Cache-Control "no-cache" always;
try_files $uri $uri/ /index.html;
}
}