Files
MABEA/backend/app/api/v1/endpoints/dokument.py
T
patrickandClaude Sonnet 5 654d448e7b
CI / backend-tests (push) Failing after 2m17s
CI / frontend-build (push) Successful in 28s
feat(dokumente): DOC-005 Zugriffsrechte je Dokumenttyp (Rechnungen eingeschränkt)
Rechnungen sind finanziell sensibel - nur Materialverantwortliche/
Leitungsverantwortliche/Administration dürfen sie in der Liste sehen und
herunterladen (403 bei direktem Downloadversuch), Mitarbeiter nicht.
Hochladen bleibt für alle offen (z.B. Wareneingang direkt scannen). Andere
Dokumenttypen bleiben unverändert für jeden mit Aktenzugriff sichtbar - die
werden im Feldeinsatz gebraucht. EINGESCHRAENKTE_DOKUMENTTYPEN als zentrale
Stelle für künftige weitere Einschränkungen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
2026-09-08 10:39:42 +02:00

129 lines
4.8 KiB
Python

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,
liste_fuer_entitaet,
loesche_dokument,
speichere_dokument,
)
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)
@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")
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")
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.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)