from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.trustedhost import TrustedHostMiddleware from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from app.core.config import settings from app.core.database import engine, Base from app.core.limiter import limiter from app.routers import auth, users, companies from app.routers import time_entries, absences, reports, ldap, smtp, caldav from app.routers import import_kimai from app.routers import kiosk from app.routers import busylight from app.routers import audit from app.routers import special_assignments @asynccontextmanager async def lifespan(app: FastAPI): # Startup: Tabellen anlegen falls noch nicht vorhanden (Alembic übernimmt das in Prod) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield # Shutdown await engine.dispose() app = FastAPI( title=settings.app_name, version="0.1.0", docs_url="/docs" if not settings.is_production else None, redoc_url="/redoc" if not settings.is_production else None, lifespan=lifespan, ) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # ── Middleware ──────────────────────────────────────────────────────────────── app.add_middleware( CORSMiddleware, allow_origins=[settings.frontend_url], allow_credentials=True, allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], allow_headers=[ "Content-Type", "Authorization", "X-Kiosk-Key-Id", "X-Kiosk-Timestamp", "X-Kiosk-Nonce", "X-Kiosk-Signature", ], ) # TrustedHostMiddleware: aktiv sobald ALLOWED_HOSTS gesetzt (Development: leer = deaktiviert) # Production: ALLOWED_HOSTS=timemaster.example.com in .env setzen if settings.allowed_hosts: app.add_middleware(TrustedHostMiddleware, allowed_hosts=settings.allowed_hosts) # ── Routers ─────────────────────────────────────────────────────────────────── API_PREFIX = "/api/v1" app.include_router(auth.router, prefix=API_PREFIX) app.include_router(users.router, prefix=API_PREFIX) app.include_router(companies.router, prefix=API_PREFIX) app.include_router(time_entries.router, prefix=API_PREFIX) app.include_router(absences.router, prefix=API_PREFIX) app.include_router(reports.router, prefix=API_PREFIX) app.include_router(ldap.router, prefix=API_PREFIX) app.include_router(smtp.router, prefix=API_PREFIX) app.include_router(caldav.router, prefix=API_PREFIX) app.include_router(import_kimai.router, prefix=API_PREFIX) app.include_router(kiosk.router, prefix=API_PREFIX) app.include_router(busylight.router, prefix=API_PREFIX) app.include_router(audit.router, prefix=API_PREFIX) app.include_router(special_assignments.router, prefix=API_PREFIX) # ── Health ──────────────────────────────────────────────────────────────────── @app.get("/health", tags=["System"]) async def health(): return {"status": "ok", "app": settings.app_name, "env": settings.app_env}