diff --git a/WEB_README.md b/WEB_README.md index 5df5f2a..2e4a99f 100644 --- a/WEB_README.md +++ b/WEB_README.md @@ -42,12 +42,38 @@ Beispiel mit Uvicorn direkt: .venv/bin/python -m uvicorn web.app:app --host 0.0.0.0 --port 8000 ``` +Optional mit App-Auth (zusätzliche Schutzschicht): +```bash +WEB_AUTH_USER=kalender WEB_AUTH_PASSWORD='StarkesPasswort' \ +.venv/bin/python -m uvicorn web.app:app --host 0.0.0.0 --port 8000 +``` + Empfohlen für Internet-Betrieb: - Uvicorn hinter Nginx - HTTPS aktivieren - Upload-Größenlimit setzen - Zugriff absichern (z. B. Basic Auth oder Login) +## App-Auth (optional, zusätzlich zu Nginx) + +Wenn `WEB_AUTH_USER` und `WEB_AUTH_PASSWORD` gesetzt sind, schützt die App alle Endpunkte per HTTP Basic Auth. + +Linux/macOS Beispiel: +```bash +export WEB_AUTH_USER=kalender +export WEB_AUTH_PASSWORD='StarkesPasswort' +./start_web.sh +``` + +Windows (PowerShell) Beispiel: +```powershell +$env:WEB_AUTH_USER='kalender' +$env:WEB_AUTH_PASSWORD='StarkesPasswort' +./start_web.cmd +``` + +Hinweis: Für öffentlich erreichbare Server weiterhin Nginx + HTTPS verwenden. + ## Öffentliches Deployment (HTTPS) Beispiel für Ubuntu-Server mit Domain `ics.example.de`. diff --git a/web/app.py b/web/app.py index dc95d00..6fb763b 100644 --- a/web/app.py +++ b/web/app.py @@ -8,9 +8,11 @@ import sys import tempfile from pathlib import Path import os +import secrets -from fastapi import FastAPI, File, Form, Request, UploadFile +from fastapi import Depends, FastAPI, File, Form, HTTPException, Request, UploadFile, status from fastapi.responses import FileResponse, HTMLResponse +from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.templating import Jinja2Templates from starlette.background import BackgroundTask @@ -22,10 +24,28 @@ from pdf_to_ics import create_ics_from_dienstplan, extract_dienstplan_data app = FastAPI(title="PDF zu ICS Web") templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates")) +security = HTTPBasic() + + +def require_auth(credentials: HTTPBasicCredentials = Depends(security)): + expected_user = os.getenv("WEB_AUTH_USER") + expected_password = os.getenv("WEB_AUTH_PASSWORD") + + if not expected_user or not expected_password: + return + + valid_user = secrets.compare_digest(credentials.username, expected_user) + valid_password = secrets.compare_digest(credentials.password, expected_password) + if not (valid_user and valid_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Unauthorized", + headers={"WWW-Authenticate": "Basic"}, + ) @app.get("/", response_class=HTMLResponse) -def index(request: Request): +def index(request: Request, _: None = Depends(require_auth)): return templates.TemplateResponse( "index.html", { @@ -41,6 +61,7 @@ async def convert_pdf( file: UploadFile = File(...), exclude_rest: bool = Form(False), exclude_vacation: bool = Form(False), + _: None = Depends(require_auth), ): filename = file.filename or "dienstplan.pdf" if not filename.lower().endswith(".pdf"): @@ -107,5 +128,5 @@ async def convert_pdf( @app.get("/health") -def health(): +def health(_: None = Depends(require_auth)): return {"status": "ok"}