d60349df67
- Neues Model SpecialAssignment mit AssignmentMode (fza|payroll|both)
- CRUD-Endpunkte unter /users/{id}/special-assignments
- Payroll-Report: GET /reports/special-assignments/payroll?year=&month=
- Migration 0029: special_assignments Tabelle + btree_gist Overlap-Constraint
- _recalculate_overtime_balance berücksichtigt FZA-Faktoren
- Frontend: Sondervertretungs-Zeiträume im UsersPage Edit-Modal
- Frontend: ReportsPage neuer Tab 'Sondervertretungen' mit Payroll-Tabelle + CSV-Export
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
89 lines
3.5 KiB
Python
89 lines
3.5 KiB
Python
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}
|