"""Engagement CRUD endpoints (flat, sprint 0).""" from __future__ import annotations from uuid import UUID from flask import Blueprint, abort, jsonify from flask.typing import ResponseReturnValue from sqlalchemy import select from mimic.api._helpers import ( audit_write, current_user_id, is_rt_lead, jsonify_model, parse_body, parse_uuid, ) from mimic.db.models import Engagement, EngagementMember from mimic.db.types import EngagementStatus from mimic.extensions import db from mimic.rbac import Permission, require_perm from mimic.schemas import EngagementCreate, EngagementRead, EngagementUpdate bp = Blueprint("engagements", __name__) def _engagement_or_404(eid: str) -> Engagement: engagement = db.session.get(Engagement, parse_uuid(eid)) if engagement is None: abort(404) # MA6: RT operators may only see engagements they're assigned to. if not is_rt_lead(): user_id = current_user_id() if user_id is None or not _is_member(engagement.id, user_id): abort(404) return engagement def _is_member(engagement_id: UUID, user_id: UUID) -> bool: stmt = select(EngagementMember).where( EngagementMember.engagement_id == engagement_id, EngagementMember.user_id == user_id, ) return db.session.execute(stmt).scalar_one_or_none() is not None @bp.get("") @require_perm(Permission.ENGAGEMENT_READ) def list_engagements() -> ResponseReturnValue: stmt = select(Engagement).order_by(Engagement.created_at.desc()) # MA6: RT operators see only their assignments. if not is_rt_lead(): user_id = current_user_id() if user_id is None: return jsonify([]) stmt = stmt.join( EngagementMember, EngagementMember.engagement_id == Engagement.id, ).where(EngagementMember.user_id == user_id) rows = db.session.execute(stmt).scalars().all() return jsonify([EngagementRead.model_validate(row).model_dump(mode="json") for row in rows]) @bp.post("") @require_perm(Permission.ENGAGEMENT_CREATE) def create_engagement() -> ResponseReturnValue: payload = parse_body(EngagementCreate) engagement = Engagement( client_name=payload.client_name, description=payload.description, c2_type=payload.c2_type, start_date=payload.start_date, end_date=payload.end_date, status=EngagementStatus.DRAFT, created_by_id=current_user_id(), ) db.session.add(engagement) db.session.commit() audit_write( action="engagement.create", resource_type="engagement", resource_id=engagement.id, metadata={"client_name": engagement.client_name}, ) return jsonify_model(EngagementRead.model_validate(engagement), status=201) @bp.get("/") @require_perm(Permission.ENGAGEMENT_READ) def get_engagement(eid: str) -> ResponseReturnValue: engagement = _engagement_or_404(eid) return jsonify_model(EngagementRead.model_validate(engagement)) @bp.put("/") @require_perm(Permission.ENGAGEMENT_UPDATE) def update_engagement(eid: str) -> ResponseReturnValue: engagement = _engagement_or_404(eid) payload = parse_body(EngagementUpdate) changes = payload.model_dump(exclude_unset=True) for field, value in changes.items(): setattr(engagement, field, value) db.session.commit() audit_write( action="engagement.update", resource_type="engagement", resource_id=engagement.id, metadata={"fields": sorted(changes.keys())}, ) return jsonify_model(EngagementRead.model_validate(engagement)) @bp.delete("/") @require_perm(Permission.ENGAGEMENT_DELETE) def delete_engagement(eid: str) -> ResponseReturnValue: engagement = _engagement_or_404(eid) engagement.status = EngagementStatus.ARCHIVED db.session.commit() audit_write( action="engagement.archive", resource_type="engagement", resource_id=engagement.id, ) return "", 204