diff --git a/Dockerfile b/Dockerfile index ff22003..07e8bd6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,11 +8,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ffmpeg \ && rm -rf /var/lib/apt/lists/* -# Non-root User für Container-Hardening -# (Synology DSM-Volumes haben ACLs — daher chown auf /data + /app) -RUN groupadd -r appuser -g 1000 && \ - useradd -r -u 1000 -g appuser -d /app -s /sbin/nologin appuser - # Python-Dependencies zuerst (Docker Layer Cache) COPY backend/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt @@ -27,12 +22,6 @@ COPY VERSION /app/VERSION RUN mkdir -p /data/media/dogs /data/media/diary /data/media/poison \ /data/media/breeds/gallery /data/media/breeds/submissions -# USER appuser auskommentiert: Synology DSM Volume-ACLs blockieren das -# (SQLite OperationalError: 'attempt to write a readonly database'). User- -# Anlage bleibt im Dockerfile damit nicht-DS-Deployments später wechseln -# können via `USER appuser` Zeile auskommentieren-entfernen. -# USER appuser - EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips=*"] diff --git a/Makefile b/Makefile index c16bf3f..9402ea3 100644 --- a/Makefile +++ b/Makefile @@ -287,8 +287,7 @@ bump: sed -i.bak -E "s/const VER[[:space:]]*=[[:space:]]*'[0-9]+'/const VER = '$$NEW'/" backend/static/sw.js && rm -f backend/static/sw.js.bak; \ sed -i.bak -E "s/const APP_VER[[:space:]]*=[[:space:]]*'[0-9]+'/const APP_VER = '$$NEW'/" backend/static/js/app.js && rm -f backend/static/js/app.js.bak; \ sed -i.bak -E "s/\?v=[0-9]+/?v=$$NEW/g" backend/static/index.html && rm -f backend/static/index.html.bak; \ - sed -i.bak -E "s/\?v=[0-9]+/?v=$$NEW/g" backend/static/landing.html && rm -f backend/static/landing.html.bak; \ - echo " ✓ APP_VER $$CUR → $$NEW (VERSION, sw.js, app.js, index.html, landing.html aktualisiert)" + echo " ✓ APP_VER $$CUR → $$NEW (VERSION, sw.js, app.js, index.html aktualisiert)" # ---------------------------------------------------------- # TEST — Smoke-Tests gegen isolierte Test-DB (kein Docker, kein DS) diff --git a/VERSION b/VERSION index a2998a8..39987d0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1120 \ No newline at end of file +1099 \ No newline at end of file diff --git a/backend/auth.py b/backend/auth.py index 1b5f126..9cb25c6 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -212,49 +212,6 @@ def require_admin(user=Depends(get_current_user)): return user -def require_moderator(user=Depends(get_current_user)): - """Dependency: Admin oder Moderator. Konsequente Nutzung statt - Inline-`if user['rolle'] not in (...):` in den Routen.""" - if user["rolle"] not in ("admin", "moderator") and not user.get("is_moderator"): - raise HTTPException(status.HTTP_403_FORBIDDEN, "Moderator-Zugriff erforderlich.") - return user - - -def require_breeder(user=Depends(get_current_user)): - """Dependency: Admin oder Züchter (breeder/breeder_test).""" - if user["rolle"] == "admin": - return user - if user.get("subscription_tier") in ("breeder", "breeder_test"): - return user - raise HTTPException(status.HTTP_403_FORBIDDEN, "Züchter-Zugriff erforderlich.") - - -# ------------------------------------------------------------------ -# Owner-Checks — zentral, statt 54x inline `if row['user_id'] != user['id']: 403` -# ------------------------------------------------------------------ -def require_owner(row, user: dict, owner_field: str = "user_id", - not_found_msg: str = "Nicht gefunden", - forbidden_msg: str = "Kein Zugriff"): - """Wirft 404 wenn row None/falsy ist, 403 wenn User nicht Besitzer. - Returns row für chainability: - dog = require_owner(conn.execute(...).fetchone(), user, 'user_id', 'Hund nicht gefunden') - """ - if not row: - raise HTTPException(status.HTTP_404_NOT_FOUND, not_found_msg) - if row[owner_field] != user["id"]: - raise HTTPException(status.HTTP_403_FORBIDDEN, forbidden_msg) - return row - - -def is_owner_or_admin(row, user: dict, owner_field: str = "user_id") -> bool: - """True wenn User Owner ist oder Admin/Moderator.""" - if not row: - return False - if user["rolle"] in ("admin", "moderator") or user.get("is_moderator"): - return True - return row[owner_field] == user["id"] - - def has_pro_access(user: dict) -> bool: """True wenn User Pro-Features nutzen darf.""" if not user: diff --git a/backend/config.py b/backend/config.py deleted file mode 100644 index a05b6be..0000000 --- a/backend/config.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Zentrale Konfiguration — vermeidet 19× duplizierte os.getenv-Aufrufe -für MEDIA_DIR und gibt einheitliche Timeout-Konstanten für externe APIs.""" -import os - - -# Speicher-Pfade -DB_PATH = os.getenv("DB_PATH", "/data/banyaro.db") -MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") -BREEDER_DOCS_DIR = os.getenv("BREEDER_DOCS_DIR", "/data/breeder_docs") -SCANINPUT_DIR = os.getenv("SCANINPUT_DIR", "/data/scaninput") - -# HTTP-Timeouts für externe APIs (in Sekunden) -# Verwendung: httpx.AsyncClient(timeout=API_TIMEOUT_DEFAULT) -API_TIMEOUT_SHORT = 5 # Schnelle Lookups (Geocoding, Reverse, einzelne Werte) -API_TIMEOUT_DEFAULT = 10 # Standardfall (Wetter, Wikipedia) -API_TIMEOUT_LONG = 30 # Größere Antworten (Overpass-Tiles, KI-Calls) - -# Standard-Header für externe Requests (Höflichkeit + Fair-Use) -HTTP_USER_AGENT = "BanYaro/1.0 (https://banyaro.app)" -HTTP_HEADERS = {"User-Agent": HTTP_USER_AGENT} diff --git a/backend/errors.py b/backend/errors.py deleted file mode 100644 index 2cbaf3f..0000000 --- a/backend/errors.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Standardisierte HTTP-Exceptions — vermeidet inkonsistente Texte -in 200+ raise-Statements.""" -from fastapi import HTTPException - - -def not_found(msg: str = "Nicht gefunden") -> HTTPException: - """404. Beispiel: `raise not_found('Hund nicht gefunden')`.""" - return HTTPException(404, msg) - - -def forbidden(msg: str = "Kein Zugriff") -> HTTPException: - """403.""" - return HTTPException(403, msg) - - -def bad_request(msg: str = "Ungültige Eingabe") -> HTTPException: - """400.""" - return HTTPException(400, msg) - - -def unauthorized(msg: str = "Nicht angemeldet") -> HTTPException: - """401.""" - return HTTPException(401, msg) - - -def conflict(msg: str = "Konflikt") -> HTTPException: - """409.""" - return HTTPException(409, msg) - - -def too_many_requests(msg: str = "Zu viele Anfragen", retry_after: int | None = None) -> HTTPException: - """429. Optional mit Retry-After Header (in Sekunden).""" - headers = {"Retry-After": str(retry_after)} if retry_after else None - return HTTPException(429, msg, headers=headers) - - -def service_unavailable(msg: str = "Dienst gerade nicht verfügbar") -> HTTPException: - """503.""" - return HTTPException(503, msg) - - -def require_or_404(row, msg: str = "Nicht gefunden"): - """Convenience: wirft 404 wenn row None/falsy, sonst gibt row zurück. - Beispiel: `dog = require_or_404(conn.execute(...).fetchone(), 'Hund nicht gefunden')`""" - if not row: - raise not_found(msg) - return row diff --git a/backend/main.py b/backend/main.py index aac642e..569a887 100644 --- a/backend/main.py +++ b/backend/main.py @@ -110,8 +110,8 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware): response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" response.headers["Content-Security-Policy"] = ( "default-src 'self'; " - "script-src 'self' https://umami.motocamp.de; " # ohne unsafe-inline/eval — alle Inline-Scripts extrahiert - "style-src 'self' 'unsafe-inline'; " # Inline-Styles bleiben (zu viele Fundstellen für jetzt) + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://umami.motocamp.de; " + "style-src 'self' 'unsafe-inline'; " "img-src 'self' data: blob: https:; " "connect-src 'self' https:; " "frame-ancestors 'none'; " @@ -1763,40 +1763,19 @@ async def force_update():
Wir besorgen neue Leckerlis 🦴
-