Files
MABEA/backend/app/api/v1/endpoints/mangel.py
T
patrickandClaude Sonnet 5 c56fc285de
CI / backend-tests (push) Failing after 1m55s
CI / frontend-build (push) Successful in 19s
feat(rollen): granulares Rechte-System (Roadmap Phase 6, letztes Modul)
Additiv neben dem bestehenden 4-Rollen-System (mitarbeiter/material-
verantwortlicher/leitungsverantwortlicher/administration bleiben unverändert,
kein Breaking Change): Admin kann jetzt eigene Rollen (z.B. "Materialwart",
"Helfer" aus der Ursprungs-Anforderung) mit frei wählbaren Einzelrechten aus
einem Berechtigungs-Katalog anlegen und Benutzern zuweisen - auch Benutzern
ganz ohne feste RolleTyp-Zuordnung.

Neue Tabellen: berechtigung (Katalog), rolle (custom, admin-anlegbar),
rolle_berechtigung (M:N), benutzer_rolle_zuordnung (M:N, eigene Tabelle statt
Wiederverwendung des ENUM-basierten benutzer_rolle).

require_roles_or_permission() kombiniert beide Systeme: bestehende feste
Rollen ODER eine passende granulare Berechtigung. Auf die vom Nutzer genannten
Beispiel-Endpunkte angewendet: Material anlegen/bearbeiten, Lagerbewegung
durchführen, Mangel melden/lesen/bearbeiten, Prüfung durchführen - weitere
Endpunkte folgen bei Bedarf nach demselben Muster (require_permission()/
require_roles_or_permission() stehen jetzt als Bausteine bereit).

Neue Endpunkte: GET /berechtigungen, CRUD /rollen, PUT/DELETE
/rollen/{id}/berechtigungen/{id}, PUT/DELETE /benutzer/{id}/rollen/{id}.

Frontend: neuer Admin-Tab "Rollen & Rechte" (Rolle anlegen, Rechte togglen,
Benutzer zuweisen/entfernen).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
2026-09-05 14:07:03 +02:00

112 lines
4.0 KiB
Python

import uuid
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import require_roles_or_permission
from app.db.session import get_db
from app.models.auth import KnotenTyp, RolleTyp, Systemknoten
from app.models.mangel import Mangel, MangelStatus
from app.models.objekt import Objekt
from app.schemas.mangel import MangelCreate, MangelRead, MangelStatusUpdate
from app.services import historie as historie_service
from app.services.mangel import aendere_status
router = APIRouter()
_ALLE_ROLLEN = (
RolleTyp.mitarbeiter,
RolleTyp.materialverantwortlicher,
RolleTyp.leitungsverantwortlicher,
RolleTyp.administration,
)
# Roadmap Phase 6: zusätzlich über eine granulare custom-Rolle (z.B. "Helfer")
# erreichbar, auch wenn diese keine der vier festen RolleTyp-Rollen trägt.
_darf_lesen = require_roles_or_permission(*_ALLE_ROLLEN, berechtigung="mangel.lesen")
_darf_melden = require_roles_or_permission(*_ALLE_ROLLEN, berechtigung="mangel.melden")
_materialverantwortliche = require_roles_or_permission(
RolleTyp.administration,
RolleTyp.materialverantwortlicher,
RolleTyp.leitungsverantwortlicher,
berechtigung="mangel.bearbeiten",
)
async def _hauptserver_id(db: AsyncSession) -> int:
result = await db.execute(select(Systemknoten.id).where(Systemknoten.typ == KnotenTyp.haupt))
return result.scalar_one()
@router.get("/maengel", response_model=list[MangelRead])
async def liste_maengel(
objekt_id: int | None = None,
status_filter: MangelStatus | None = None,
db: AsyncSession = Depends(get_db),
_=Depends(_darf_lesen),
) -> list[Mangel]:
"""Prompt-Erweiterung (Mangel-Modul): jeder Mitarbeiter+ darf lesen (Mängel
sind für alle im Einsatzalltag relevant), Status ändern bleibt
Materialverantwortlichen+ vorbehalten (Melden vs. Bearbeiten getrennt)."""
stmt = select(Mangel)
if objekt_id is not None:
stmt = stmt.where(Mangel.objekt_id == objekt_id)
if status_filter is not None:
stmt = stmt.where(Mangel.status == status_filter)
stmt = stmt.order_by(Mangel.gemeldet_am.desc())
result = await db.execute(stmt)
return list(result.scalars().all())
@router.post("/maengel", response_model=MangelRead, status_code=status.HTTP_201_CREATED)
async def melde_mangel(
payload: MangelCreate, db: AsyncSession = Depends(get_db), current_user=Depends(_darf_melden)
) -> Mangel:
objekt = await db.get(Objekt, payload.objekt_id)
if objekt is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekt nicht gefunden")
mangel = Mangel(
objekt_id=payload.objekt_id,
entitaet_typ=payload.entitaet_typ,
entitaet_id=payload.entitaet_id,
beschreibung=payload.beschreibung,
prioritaet=payload.prioritaet,
gemeldet_von=current_user.id,
gemeldet_am=datetime.now(timezone.utc),
)
db.add(mangel)
await db.flush()
await historie_service.log(
db,
zustaendiger_server_id=await _hauptserver_id(db),
benutzer_id=current_user.id,
ereignistyp="mangel_gemeldet",
entitaet_typ="mangel",
entitaet_id=mangel.id,
neuer_wert={"beschreibung": mangel.beschreibung, "prioritaet": mangel.prioritaet.value},
)
return mangel
@router.patch("/maengel/{mangel_id}", response_model=MangelRead)
async def aendere_mangel_status(
mangel_id: uuid.UUID,
payload: MangelStatusUpdate,
db: AsyncSession = Depends(get_db),
current_user=Depends(_materialverantwortliche),
) -> Mangel:
mangel = await db.get(Mangel, mangel_id)
if mangel is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mangel nicht gefunden")
return await aendere_status(
db,
mangel=mangel,
status=payload.status,
benutzer_id=current_user.id,
zustaendiger_server_id=await _hauptserver_id(db),
)