from pathlib import Path from dotenv import load_dotenv import os ENV_PATH = Path(__file__).resolve().parent.parent / ".env" os.environ["DOTENV_PATH"] = str(ENV_PATH) load_dotenv(dotenv_path=ENV_PATH, override=True) from fastapi import FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from starlette.middleware.base import BaseHTTPMiddleware from app.api import ( auth, users, products, bookings, deliveries, stats, topups, audit_logs, profile, transactions, categories, ledger, admin_settings, paypal_ipn ) from app.core.auth import CSRF_HEADER_NAME, verify_csrf ALLOWED_ORIGINS = ["http://localhost:5173"] # CSRF-Ausnahmen (nur für DEV sinnvoll!) CSRF_EXEMPT = { "/auth/pin-login", "/auth/management-login", "/auth/logout", "/auth/csrf", "/docs", "/openapi.json", "/redoc", "/docs/oauth2-redirect", } # Swagger-/Redoc-Pfade DOCS_PATHS = ("/docs", "/redoc", "/openapi.json") app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=ALLOWED_ORIGINS, allow_credentials=True, allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], allow_headers=["Content-Type", CSRF_HEADER_NAME], ) class SecurityHeadersMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): path = request.url.path # CSRF nur für mutierende Methoden prüfen, ausgenommene Pfade überspringen if ( request.method in {"POST", "PUT", "PATCH", "DELETE"} and not any(path == p or path.startswith(p + "/") for p in CSRF_EXEMPT) ): verify_csrf(request) response: Response = await call_next(request) if any(path == p or path.startswith(p + "/") for p in DOCS_PATHS): response.headers.setdefault( "Content-Security-Policy", "default-src 'self' https:; " "img-src 'self' data: https:; " "style-src 'self' 'unsafe-inline' https:; " "script-src 'self' 'unsafe-inline' https:" ) return response # Normale Security-Header + strikte CSP response.headers.setdefault("X-Frame-Options", "DENY") response.headers.setdefault("X-Content-Type-Options", "nosniff") response.headers.setdefault("Referrer-Policy", "no-referrer") response.headers.setdefault( "Content-Security-Policy", "default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'" ) return response app.add_middleware(SecurityHeadersMiddleware) # Medienordner sicherstellen (verhindert RuntimeError beim Mount) os.makedirs("media/avatars", exist_ok=True) # Static files für Avatare & Medien app.mount("/media", StaticFiles(directory="media"), name="media") # Router registrieren app.include_router(auth.router) app.include_router(users.router) app.include_router(products.router) app.include_router(bookings.router) app.include_router(deliveries.router) app.include_router(stats.router) app.include_router(topups.router) app.include_router(audit_logs.router) app.include_router(profile.router) app.include_router(transactions.router) app.include_router(categories.router) app.include_router(ledger.router) app.include_router(admin_settings.router) app.include_router(paypal_ipn.router) # app.include_router(sessions.router) @app.get("/") def root(): return {"message": "Bacchus API läuft"}