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 from app.routers import hours_payouts @asynccontextmanager async def lifespan(app: FastAPI): # Startup: Tabellen anlegen nur in Development/Test. # In Production verwaltet Alembic das Schema – create_all würde mit Migrationen kollidieren. import logging _log = logging.getLogger(__name__) if not settings.is_production: async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) _log.info("Development-Modus: create_all ausgeführt.") else: _log.info("Production-Modus: create_all übersprungen — Alembic verwaltet das Schema.") 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) app.include_router(hours_payouts.router, prefix=API_PREFIX) # ── Health ──────────────────────────────────────────────────────────────────── @app.get("/health", tags=["System"]) async def health(): return {"status": "ok", "app": settings.app_name, "env": settings.app_env}