diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index 719cc41..0ba45b9 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -19,7 +19,7 @@ from app.schemas.disclosure import ( 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, + get_pending_count, review_disclosure_request, revoke_disclosure, ) router = APIRouter() @@ -316,12 +316,10 @@ def list_disclosure_requests( admin: User = Depends(require_admin), db: Session = Depends(get_db), ): - """List disclosure requests (default: pending only).""" + """List disclosure requests. Without status filter, returns all.""" 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() @@ -382,3 +380,34 @@ def review_disclosure( reviewed_by=dr.reviewed_by, reviewed_at=dr.reviewed_at, expires_at=dr.expires_at, created_at=dr.created_at, ) + + +@router.put("/disclosure-requests/{request_id}/revoke", response_model=DisclosureRequestResponse) +def admin_revoke_disclosure( + request_id: int, + request: Request, + admin: User = Depends(require_admin), + db: Session = Depends(get_db), +): + """Admin: revoke an active disclosure (sets expires_at to now).""" + try: + dr = revoke_disclosure(db, request_id, admin.id, admin=True) + except ValueError as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + + log_action( + db, user_id=admin.id, action="disclosure_admin_revoked", + entity_type="disclosure_request", entity_id=request_id, + new_values={"expires_at": dr.expires_at.isoformat() if dr.expires_at else None}, + 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, + ) diff --git a/backend/app/api/cases.py b/backend/app/api/cases.py index 73ce76c..6c96bac 100644 --- a/backend/app/api/cases.py +++ b/backend/app/api/cases.py @@ -26,7 +26,8 @@ from app.config import get_settings from app.services.audit_service import log_action from app.services.import_service import has_random_suffix from app.services.disclosure_service import ( - create_disclosure_request, has_active_disclosure, has_pending_request, + create_disclosure_request, get_my_disclosure_requests, has_active_disclosure, + has_pending_request, revoke_disclosure, ) from app.services.icd_service import generate_coding_template, get_pending_icd_cases, save_icd_for_case @@ -205,6 +206,62 @@ def request_disclosure( ) +# --------------------------------------------------------------------------- +# My disclosure requests (for dak_mitarbeiter) +# --------------------------------------------------------------------------- + + +@router.get("/my-disclosure-requests", response_model=list[DisclosureRequestResponse]) +def list_my_disclosure_requests( + db: Session = Depends(get_db), + user: User = Depends(get_current_user), +): + """Return all disclosure requests made by the current user.""" + requests = get_my_disclosure_requests(db, user.id) + 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.put("/disclosure-requests/{request_id}/revoke", response_model=DisclosureRequestResponse) +def revoke_my_disclosure( + request_id: int, + request: Request, + db: Session = Depends(get_db), + user: User = Depends(get_current_user), +): + """Revoke own active disclosure (sets expires_at to now).""" + try: + dr = revoke_disclosure(db, request_id, user.id, admin=False) + except ValueError as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + + log_action( + db, user_id=user.id, action="disclosure_self_revoked", + entity_type="disclosure_request", entity_id=request_id, + new_values={"expires_at": dr.expires_at.isoformat() if dr.expires_at else None}, + 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, + ) + + # --------------------------------------------------------------------------- # Paginated case list # --------------------------------------------------------------------------- diff --git a/backend/app/services/disclosure_service.py b/backend/app/services/disclosure_service.py index b553b5a..4a9c783 100644 --- a/backend/app/services/disclosure_service.py +++ b/backend/app/services/disclosure_service.py @@ -113,6 +113,38 @@ def review_disclosure_request( return dr +def revoke_disclosure(db: Session, request_id: int, user_id: int, *, admin: bool = False) -> DisclosureRequest: + """Revoke an active disclosure by setting expires_at to now. + + If admin=False, only the original requester can revoke their own disclosure. + If admin=True, any admin can revoke any disclosure. + """ + dr = db.query(DisclosureRequest).filter(DisclosureRequest.id == request_id).first() + if not dr: + raise ValueError("Disclosure request not found") + if dr.status != "approved": + raise ValueError("Only approved disclosures can be revoked") + if dr.expires_at and dr.expires_at <= datetime.now(timezone.utc): + raise ValueError("Disclosure already expired") + if not admin and dr.requester_id != user_id: + raise ValueError("Not authorized to revoke this disclosure") + + dr.expires_at = datetime.now(timezone.utc) + db.commit() + db.refresh(dr) + return dr + + +def get_my_disclosure_requests(db: Session, user_id: int) -> list[DisclosureRequest]: + """Return all disclosure requests made by a specific user.""" + return ( + db.query(DisclosureRequest) + .filter(DisclosureRequest.requester_id == user_id) + .order_by(DisclosureRequest.created_at.desc()) + .all() + ) + + def get_pending_requests(db: Session) -> list[DisclosureRequest]: """Return all pending disclosure requests.""" return ( diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9bc4ae5..c9347b6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,6 +16,8 @@ import { AdminInvitationsPage } from '@/pages/AdminInvitationsPage' import { AdminAuditPage } from '@/pages/AdminAuditPage' import { DisclosuresPage } from '@/pages/DisclosuresPage' import { AccountPage } from '@/pages/AccountPage' +import { GutachtenStatistikPage } from '@/pages/GutachtenStatistikPage' +import { MyDisclosuresPage } from '@/pages/MyDisclosuresPage' const queryClient = new QueryClient({ defaultOptions: { @@ -43,7 +45,9 @@ function App() { } /> } /> } /> + } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 7f174ce..dec7964 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -36,6 +36,7 @@ const mainNavItems: NavItem[] = [ const accountNavItems: NavItem[] = [ { label: 'Kontoverwaltung', to: '/account', icon: UserCog }, + { label: 'Meine Freigaben', to: '/disclosures', icon: ShieldCheck, mitarbeiterOnly: true }, ] const adminNavItems: NavItem[] = [ @@ -68,11 +69,15 @@ function NavItemLink({ item }: { item: NavItem }) { export function Sidebar({ className }: { className?: string }) { const { user, isAdmin } = useAuth() - const visibleMainItems = mainNavItems.filter((item) => { - if (item.adminOnly && !isAdmin) return false - if (item.mitarbeiterOnly && user?.role !== 'dak_mitarbeiter') return false - return true - }) + const filterItems = (items: NavItem[]) => + items.filter((item) => { + if (item.adminOnly && !isAdmin) return false + if (item.mitarbeiterOnly && user?.role !== 'dak_mitarbeiter') return false + return true + }) + + const visibleMainItems = filterItems(mainNavItems) + const visibleAccountItems = filterItems(accountNavItems) return (