Files
timemaster/backend/app/main.py
T
patrickandClaude Opus 4.8 43ebc83905
Security Audit / Node.js Dependency Audit (push) Has been cancelled
Security Audit / Python Dependency Audit (push) Has been cancelled
feat(ical): abonnierbarer read-only Kalender-Feed pro Nutzer
Token-gescoper iCal-Feed (/absences/ical/<token>.ics), abonnierbar in
Outlook/Apple/Google. Anders als der CalDAV-Client (Push nach Nextcloud)
pollt der Kalender die URL selbst. Feed zeigt nur die eigenen bestätigten
Abwesenheiten des Token-Inhabers.

- users.ical_token_hash (SHA-256, rotierbar) + Migration 0041 (nur Spalte,
  keine RLS-Aenderung; users-Policy deckt neue nullable Spalte ab)
- Router ical.py: oeffentlicher Feed (kein JWT) + Token-Verwaltung
  POST/GET/DELETE /users/me/ical-token (authentifiziert)
- ProfilePage: Sektion "Kalender-Abo (iCal)" mit Erzeugen/Rotieren/
  Deaktivieren, URL-Anzeige einmalig + Kopieren
- test_ical.py: Token-Lifecycle + oeffentlicher Feed (3 Tests)

Deployed auf 137 (Migration 0041, 196/196 Tests gruen). 164 ausstehend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:12:05 +02:00

122 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
from app.routers import public_stamp
from app.routers import ical
from app.routers import reseller, tenants
@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.")
# M-4: Warnung wenn Production ohne ALLOWED_HOSTS läuft
if settings.is_production and not settings.allowed_hosts:
_log.warning(
"SICHERHEITSWARNUNG: ALLOWED_HOSTS ist nicht gesetzt. "
"In Production sollte ALLOWED_HOSTS in .env konfiguriert sein "
"um Host-Header-Injection zu verhindern."
)
# Erinnerungs-Scheduler (agent-11 PR3) nicht im Test-Kontext starten
import sys
if settings.scheduler_enabled and "pytest" not in sys.modules:
from app.services.scheduler_service import start as start_scheduler
start_scheduler()
yield
# Shutdown
from app.services.scheduler_service import shutdown as shutdown_scheduler
shutdown_scheduler()
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(public_stamp.router, prefix=API_PREFIX)
app.include_router(ical.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)
app.include_router(reseller.router, prefix=API_PREFIX)
app.include_router(tenants.router, prefix=API_PREFIX)
# ── Health ────────────────────────────────────────────────────────────────────
@app.get("/health", tags=["System"])
async def health():
return {"status": "ok", "app": settings.app_name, "env": settings.app_env}