mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 16:03:41 +00:00
feat: add case masking for dak_mitarbeiter and disclosure endpoints
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
00076e2c00
commit
9825489781
2 changed files with 170 additions and 21 deletions
|
|
@ -11,10 +11,16 @@ from sqlalchemy.orm import Session
|
|||
from app.core.dependencies import require_admin
|
||||
from app.core.security import hash_password
|
||||
from app.database import get_db
|
||||
from app.models.audit import AuditLog
|
||||
from app.models.audit import AuditLog, DisclosureRequest
|
||||
from app.models.user import InvitationLink, User
|
||||
from app.schemas.disclosure import (
|
||||
DisclosureCountResponse, DisclosureRequestResponse, DisclosureRequestUpdate,
|
||||
)
|
||||
from app.schemas.user import UserCreate, UserResponse, UserUpdate
|
||||
from app.services.audit_service import log_action
|
||||
from app.services.disclosure_service import (
|
||||
get_pending_count, review_disclosure_request,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
|
@ -298,3 +304,81 @@ def get_audit_log(
|
|||
.all()
|
||||
)
|
||||
return entries
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Disclosure requests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/disclosure-requests", response_model=list[DisclosureRequestResponse])
|
||||
def list_disclosure_requests(
|
||||
status_filter: Optional[str] = Query(None, alias="status"),
|
||||
admin: User = Depends(require_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""List disclosure requests (default: pending only)."""
|
||||
query = db.query(DisclosureRequest)
|
||||
if status_filter:
|
||||
query = query.filter(DisclosureRequest.status == status_filter)
|
||||
else:
|
||||
query = query.filter(DisclosureRequest.status == "pending")
|
||||
|
||||
requests = query.order_by(DisclosureRequest.created_at.desc()).all()
|
||||
|
||||
result = []
|
||||
for dr in requests:
|
||||
result.append(DisclosureRequestResponse(
|
||||
id=dr.id, case_id=dr.case_id, requester_id=dr.requester_id,
|
||||
requester_username=dr.requester.username if dr.requester else None,
|
||||
fall_id=dr.case.fall_id if dr.case else None,
|
||||
reason=dr.reason, status=dr.status,
|
||||
reviewed_by=dr.reviewed_by, reviewed_at=dr.reviewed_at,
|
||||
expires_at=dr.expires_at, created_at=dr.created_at,
|
||||
))
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/disclosure-requests/count", response_model=DisclosureCountResponse)
|
||||
def disclosure_request_count(
|
||||
admin: User = Depends(require_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Return count of pending disclosure requests."""
|
||||
return DisclosureCountResponse(pending_count=get_pending_count(db))
|
||||
|
||||
|
||||
@router.put("/disclosure-requests/{request_id}", response_model=DisclosureRequestResponse)
|
||||
def review_disclosure(
|
||||
request_id: int,
|
||||
payload: DisclosureRequestUpdate,
|
||||
request: Request,
|
||||
admin: User = Depends(require_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Approve or reject a disclosure request."""
|
||||
if payload.status not in ("approved", "rejected"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="Status must be 'approved' or 'rejected'",
|
||||
)
|
||||
try:
|
||||
dr = review_disclosure_request(db, request_id, admin.id, payload.status)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
log_action(
|
||||
db, user_id=admin.id, action=f"disclosure_{payload.status}",
|
||||
entity_type="disclosure_request", entity_id=request_id,
|
||||
new_values={"status": payload.status, "case_id": dr.case_id},
|
||||
ip_address=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("user-agent"),
|
||||
)
|
||||
|
||||
return DisclosureRequestResponse(
|
||||
id=dr.id, case_id=dr.case_id, requester_id=dr.requester_id,
|
||||
requester_username=dr.requester.username if dr.requester else None,
|
||||
fall_id=dr.case.fall_id if dr.case else None,
|
||||
reason=dr.reason, status=dr.status,
|
||||
reviewed_by=dr.reviewed_by, reviewed_at=dr.reviewed_at,
|
||||
expires_at=dr.expires_at, created_at=dr.created_at,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,9 +19,14 @@ from app.schemas.case import (
|
|||
CaseUpdate,
|
||||
CodingUpdate,
|
||||
ICDUpdate,
|
||||
mask_case_for_mitarbeiter,
|
||||
)
|
||||
from app.schemas.disclosure import DisclosureRequestCreate, DisclosureRequestResponse
|
||||
from app.config import get_settings
|
||||
from app.services.audit_service import log_action
|
||||
from app.services.disclosure_service import (
|
||||
create_disclosure_request, has_active_disclosure, has_pending_request,
|
||||
)
|
||||
from app.services.icd_service import generate_coding_template, get_pending_icd_cases, save_icd_for_case
|
||||
|
||||
settings = get_settings()
|
||||
|
|
@ -31,6 +36,17 @@ logger = logging.getLogger(__name__)
|
|||
router = APIRouter()
|
||||
|
||||
|
||||
def _mask_case_response(case: Case, user: User, db: Session) -> dict:
|
||||
"""Serialize a single case, masking sensitive fields for dak_mitarbeiter."""
|
||||
case_dict = CaseResponse.model_validate(case).model_dump()
|
||||
if user.role != "admin":
|
||||
granted, expires_at = has_active_disclosure(db, case.id, user.id)
|
||||
case_dict = mask_case_for_mitarbeiter(case_dict, disclosure_granted=granted)
|
||||
if expires_at:
|
||||
case_dict["disclosure_expires_at"] = expires_at.isoformat()
|
||||
return case_dict
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Static routes MUST be defined BEFORE the parameterised /{case_id} routes
|
||||
# to avoid FastAPI interpreting "pending-icd" etc. as a case_id.
|
||||
|
|
@ -53,12 +69,11 @@ def list_pending_icd(
|
|||
cases, total = get_pending_icd_cases(
|
||||
db, jahr=jahr, fallgruppe=fallgruppe, page=page, per_page=per_page
|
||||
)
|
||||
return CaseListResponse(
|
||||
items=cases,
|
||||
total=total,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
)
|
||||
if user.role == "admin":
|
||||
return CaseListResponse(items=cases, total=total, page=page, per_page=per_page)
|
||||
|
||||
masked_items = [_mask_case_response(c, user, db) for c in cases]
|
||||
return {"items": masked_items, "total": total, "page": page, "per_page": per_page}
|
||||
|
||||
|
||||
@router.get("/pending-coding", response_model=CaseListResponse)
|
||||
|
|
@ -146,6 +161,47 @@ def download_coding_template(
|
|||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Disclosure request
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/{case_id}/disclosure-request", response_model=DisclosureRequestResponse)
|
||||
def request_disclosure(
|
||||
case_id: int,
|
||||
payload: DisclosureRequestCreate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Request disclosure of personal data for a case."""
|
||||
case = db.query(Case).filter(Case.id == case_id).first()
|
||||
if not case:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Case not found")
|
||||
|
||||
if has_pending_request(db, case_id, user.id):
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, "Anfrage bereits ausstehend")
|
||||
|
||||
granted, _ = has_active_disclosure(db, case_id, user.id)
|
||||
if granted:
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, "Freigabe bereits aktiv")
|
||||
|
||||
dr = create_disclosure_request(db, case_id, user.id, payload.reason)
|
||||
|
||||
log_action(
|
||||
db, user_id=user.id, action="disclosure_requested",
|
||||
entity_type="case", entity_id=case_id,
|
||||
new_values={"reason": payload.reason},
|
||||
ip_address=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("user-agent"),
|
||||
)
|
||||
|
||||
return DisclosureRequestResponse(
|
||||
id=dr.id, case_id=dr.case_id, requester_id=dr.requester_id,
|
||||
reason=dr.reason, status=dr.status, created_at=dr.created_at,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Paginated case list
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -194,14 +250,22 @@ def list_cases(
|
|||
query = query.filter(Case.gutachten_typ == None) # noqa: E711
|
||||
if search:
|
||||
like_pattern = f"%{search}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
Case.nachname.ilike(like_pattern),
|
||||
Case.vorname.ilike(like_pattern),
|
||||
Case.fall_id.ilike(like_pattern),
|
||||
Case.kvnr.ilike(like_pattern),
|
||||
if user.role == "admin":
|
||||
query = query.filter(
|
||||
or_(
|
||||
Case.nachname.ilike(like_pattern),
|
||||
Case.vorname.ilike(like_pattern),
|
||||
Case.fall_id.ilike(like_pattern),
|
||||
Case.kvnr.ilike(like_pattern),
|
||||
)
|
||||
)
|
||||
else:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Case.fall_id.ilike(like_pattern),
|
||||
Case.kvnr.ilike(like_pattern),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
cases = (
|
||||
|
|
@ -211,12 +275,11 @@ def list_cases(
|
|||
.all()
|
||||
)
|
||||
|
||||
return CaseListResponse(
|
||||
items=cases,
|
||||
total=total,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
)
|
||||
if user.role == "admin":
|
||||
return CaseListResponse(items=cases, total=total, page=page, per_page=per_page)
|
||||
|
||||
masked_items = [_mask_case_response(c, user, db) for c in cases]
|
||||
return {"items": masked_items, "total": total, "page": page, "per_page": per_page}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -237,7 +300,9 @@ def get_case(
|
|||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Case not found",
|
||||
)
|
||||
return case
|
||||
if user.role == "admin":
|
||||
return case
|
||||
return _mask_case_response(case, user, db)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue