import uuid from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user, require_roles from app.db.session import get_db from app.models.auth import Benutzer, RolleTyp from app.models.dokument import EINGESCHRAENKTE_DOKUMENTTYPEN, Dokument, DokumentTyp from app.schemas.dokument import DokumentRead, EntitaetTyp from app.services.dokument import ( DateityperlaubtError, DateizugrossError, DokumentDuplikatError, dateipfad, ersetze_dokument, liste_fuer_entitaet, loesche_dokument, speichere_dokument, versionshistorie, ) router = APIRouter() _mitarbeiter_plus = require_roles( RolleTyp.mitarbeiter, RolleTyp.materialverantwortlicher, RolleTyp.leitungsverantwortlicher, RolleTyp.administration, ) _materialverantwortliche = require_roles( RolleTyp.administration, RolleTyp.materialverantwortlicher, RolleTyp.leitungsverantwortlicher ) _VERANTWORTLICHE_ROLLEN = {RolleTyp.administration, RolleTyp.materialverantwortlicher, RolleTyp.leitungsverantwortlicher} def _darf_eingeschraenkte_dokumente_sehen(benutzer: Benutzer) -> bool: """DOC-005: Rechnungen etc. nur für Materialverantwortliche+ - siehe EINGESCHRAENKTE_DOKUMENTTYPEN.""" return bool({RolleTyp(r) for r in benutzer.rollen_namen} & _VERANTWORTLICHE_ROLLEN) def _pruefe_eingeschraenkten_zugriff(dokument: Dokument, current_user: Benutzer) -> None: if dokument.dokumenttyp in EINGESCHRAENKTE_DOKUMENTTYPEN and not _darf_eingeschraenkte_dokumente_sehen( current_user ): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Für diesen Dokumenttyp fehlt die Berechtigung") @router.get("/dokumente", response_model=list[DokumentRead]) async def liste_dokumente( entitaet_typ: EntitaetTyp, entitaet_id: str, dokumenttyp: DokumentTyp | None = None, db: AsyncSession = Depends(get_db), current_user: Benutzer = Depends(get_current_user), ) -> list[Dokument]: return await liste_fuer_entitaet( db, entitaet_typ=entitaet_typ, entitaet_id=entitaet_id, dokumenttyp=dokumenttyp, darf_eingeschraenkte_sehen=_darf_eingeschraenkte_dokumente_sehen(current_user), ) @router.post("/dokumente", response_model=DokumentRead, status_code=status.HTTP_201_CREATED) async def lade_dokument_hoch( entitaet_typ: EntitaetTyp = Form(...), entitaet_id: str = Form(...), dokumenttyp: DokumentTyp = Form(...), beschreibung: str | None = Form(None), datei: UploadFile = File(...), db: AsyncSession = Depends(get_db), current_user=Depends(_mitarbeiter_plus), ) -> Dokument: inhalt = await datei.read() try: return await speichere_dokument( db, entitaet_typ=entitaet_typ, entitaet_id=entitaet_id, dateiname=datei.filename or "unbenannt", mime_type=datei.content_type or "application/octet-stream", inhalt=inhalt, dokumenttyp=dokumenttyp, beschreibung=beschreibung, hochgeladen_von=current_user.id, ) except DateityperlaubtError as exc: raise HTTPException( status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, detail=f"Dateityp nicht erlaubt: {exc}" ) from exc except DateizugrossError as exc: raise HTTPException( status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail="Datei zu groß" ) from exc except DokumentDuplikatError as exc: bestehendes = exc.bestehendes_dokument raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=( f"Diese Datei wurde bereits am {bestehendes.hochgeladen_am:%d.%m.%Y %H:%M} " f"als „{bestehendes.dateiname}“ hochgeladen." ), ) from exc @router.get("/dokumente/{dokument_id}/download") async def lade_dokument_herunter( dokument_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: Benutzer = Depends(get_current_user), ) -> FileResponse: dokument = await db.get(Dokument, dokument_id) if dokument is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dokument nicht gefunden") _pruefe_eingeschraenkten_zugriff(dokument, current_user) pfad = dateipfad(dokument) if not pfad.exists(): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Datei nicht mehr vorhanden") return FileResponse(pfad, media_type=dokument.mime_type, filename=dokument.dateiname) @router.post("/dokumente/{dokument_id}/ersetzen", response_model=DokumentRead, status_code=status.HTTP_201_CREATED) async def dokument_ersetzen( dokument_id: uuid.UUID, beschreibung: str | None = Form(None), datei: UploadFile = File(...), db: AsyncSession = Depends(get_db), current_user=Depends(_mitarbeiter_plus), ) -> Dokument: """DOC-003: neue Version anlegen statt zu überschreiben - entitaet_typ/ entitaet_id/dokumenttyp werden vom Vorgänger übernommen.""" altes_dokument = await db.get(Dokument, dokument_id) if altes_dokument is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dokument nicht gefunden") _pruefe_eingeschraenkten_zugriff(altes_dokument, current_user) inhalt = await datei.read() try: return await ersetze_dokument( db, altes_dokument=altes_dokument, dateiname=datei.filename or "unbenannt", mime_type=datei.content_type or "application/octet-stream", inhalt=inhalt, beschreibung=beschreibung, hochgeladen_von=current_user.id, ) except DateityperlaubtError as exc: raise HTTPException( status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, detail=f"Dateityp nicht erlaubt: {exc}" ) from exc except DateizugrossError as exc: raise HTTPException( status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail="Datei zu groß" ) from exc except DokumentDuplikatError as exc: bestehendes = exc.bestehendes_dokument raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=( f"Diese Datei wurde bereits am {bestehendes.hochgeladen_am:%d.%m.%Y %H:%M} " f"als „{bestehendes.dateiname}“ hochgeladen." ), ) from exc @router.get("/dokumente/{dokument_id}/versionen", response_model=list[DokumentRead]) async def dokument_versionen( dokument_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: Benutzer = Depends(get_current_user), ) -> list[Dokument]: """DOC-003: komplette Versionskette (neueste zuerst, inkl. übergebenem Dokument selbst).""" dokument = await db.get(Dokument, dokument_id) if dokument is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dokument nicht gefunden") _pruefe_eingeschraenkten_zugriff(dokument, current_user) return await versionshistorie(db, dokument) @router.delete("/dokumente/{dokument_id}", status_code=status.HTTP_204_NO_CONTENT) async def entferne_dokument( dokument_id: uuid.UUID, db: AsyncSession = Depends(get_db), _=Depends(_materialverantwortliche) ) -> None: dokument = await db.get(Dokument, dokument_id) if dokument is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dokument nicht gefunden") await loesche_dokument(db, dokument=dokument)