from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, ConfigDict, Field from ..db.session import get_session from ..services.audit_logger import log_event from ..services.action_execution import approval_execution_supported, execute_approved_action from ..services.approval_service import ( ApprovalStateError, approval_fingerprint, create_approval, dismiss_expired_approvals, get_approval, list_approvals, mark_approval_executed, update_approval_status, ) router = APIRouter(prefix='/api/approvals') class ApprovalCreateRequest(BaseModel): model_config = ConfigDict(extra='forbid') action: str = Field(min_length=1, max_length=160) target: str = Field(min_length=1, max_length=240) source: str = Field(default='manual', pattern='^(manual|voice|automation|system)$') risk: str = Field(default='medium', pattern='^(low|medium|high|critical)$') reason: str | None = Field(default=None, max_length=1000) requester: str = Field(default='manual', max_length=120) plan: list[str] = Field(default_factory=list, max_length=30) class ApprovalDecisionRequest(BaseModel): model_config = ConfigDict(extra='forbid') status: str = Field(min_length=1, max_length=32) @router.get('') def list_all(include_dismissed: bool = Query(default=False), session=Depends(get_session)): return [ { **approval.model_dump(), 'executionSupported': approval_execution_supported(approval.action), 'executionReason': None if approval_execution_supported(approval.action) else 'Geen rollback-veilige executor geregistreerd.', } for approval in list_approvals(session, include_dismissed=include_dismissed) ] @router.post('/cleanup') def cleanup_expired(session=Depends(get_session)): dismissed = dismiss_expired_approvals(session) audit = log_event( session, source='manual', identity='operator', action_id='approval.cleanup', target='approval-queue', target_type='approval', risk='low', policy_decision='allowed', reason=f'{len(dismissed)} expired approval(s) archived; active approvals were retained.', result='success', execution_plan={'steps': ['expire overdue pending approvals', 'archive expired approvals', 'retain approval and audit records']}, redacted_metadata={'dismissedCount': len(dismissed)}, ) return { 'success': True, 'message': f'{len(dismissed)} verlopen approval(s) opgeruimd.', 'dismissedCount': len(dismissed), 'auditId': audit.id, } @router.post('') def create(payload: ApprovalCreateRequest, session=Depends(get_session)): approval = create_approval( session, action=payload.action, target=payload.target, source=payload.source, risk=payload.risk, reason=payload.reason, requester=payload.requester, context={'plan': payload.plan}, ) return {'success': True, 'message': 'Approval created', 'id': approval.id} @router.patch('/{approval_id}') def update(approval_id: str, payload: ApprovalDecisionRequest, session=Depends(get_session)): status = payload.status try: updated = update_approval_status(session, approval_id, status) except ApprovalStateError as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc if not updated: return {'success': False, 'message': 'Approval not found', 'id': approval_id} audit = log_event( session, source='manual', identity='operator', action_id=f'approval.{status}', target=updated.target, target_type='approval', risk=updated.risk, policy_decision='approval_required', reason=updated.reason or f'Approval {approval_id} marked {status}.', result='success', execution_plan={'steps': ['load approval', f'mark {status}', 'write audit']}, redacted_metadata={'approvalId': updated.id, 'approvalStatus': updated.status, 'requestedAction': updated.action}, ) return { 'success': True, 'id': updated.id, 'status': updated.status, 'auditId': audit.id, 'message': f'Approval {updated.status}', } @router.post('/{approval_id}/execute') def execute_approved(approval_id: str, session=Depends(get_session)): approval = get_approval(session, approval_id) if not approval: raise HTTPException(status_code=404, detail='Approval not found.') if approval.status != 'approved': raise HTTPException(status_code=409, detail='Only an approved request can be executed.') expected = approval_fingerprint(approval.action, approval.target, approval.source, approval.risk, approval.request_context_json) if not approval.action_fingerprint or approval.action_fingerprint != expected: raise HTTPException(status_code=409, detail='Approval action context changed after review.') result = execute_approved_action(session, approval) if not result.get('success'): return result mark_approval_executed( session, approval, job_id=str(result['jobId']), audit_event_id=str(result['auditId']), ) return { **result, 'approvalId': approval.id, 'approvalStatus': 'executed', }