import json from datetime import datetime from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, status from pydantic import BaseModel from api.db import db_client from api.db.models import UserModel from api.services.auth.depends import get_superuser from api.services.auth.stack_auth import stackauth router = APIRouter(prefix="/superuser", tags=["superuser"]) class ImpersonateRequest(BaseModel): """Request payload for superadmin impersonation. Either ``provider_user_id`` **or** ``user_id`` must be supplied. If both are provided, ``provider_user_id`` takes precedence. """ provider_user_id: str | None = None user_id: int | None = None class ImpersonateResponse(BaseModel): refresh_token: str access_token: str class SuperuserWorkflowRunResponse(BaseModel): id: int name: str workflow_id: int workflow_name: Optional[str] user_id: Optional[int] organization_id: Optional[int] organization_name: Optional[str] mode: str is_completed: bool recording_url: Optional[str] transcript_url: Optional[str] usage_info: Optional[dict] cost_info: Optional[dict] initial_context: Optional[dict] gathered_context: Optional[dict] admin_comment: Optional[str] admin_comment_ts: Optional[datetime] created_at: datetime class SuperuserWorkflowRunsListResponse(BaseModel): workflow_runs: List[SuperuserWorkflowRunResponse] total_count: int page: int limit: int total_pages: int @router.post("/impersonate") async def impersonate( request: ImpersonateRequest, user: UserModel = Depends(get_superuser) ) -> ImpersonateResponse: """Impersonate a user as a super-admin. Internally, Stack Auth requires the **provider user ID** (a UUID-ish string) to create an impersonation session. """ provider_user_id: str | None = request.provider_user_id # ------------------------------------------------------------------ # Fallback: resolve provider_user_id from internal ``user_id`` # ------------------------------------------------------------------ if provider_user_id is None: if request.user_id is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Either 'provider_user_id' or 'user_id' must be provided.", ) db_user = await db_client.get_user_by_id(request.user_id) if db_user is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"User with ID {request.user_id} not found.", ) provider_user_id = db_user.provider_id # ------------------------------------------------------------------ # Call Stack Auth to create the impersonation session # ------------------------------------------------------------------ session = await stackauth.impersonate(provider_user_id) return ImpersonateResponse( refresh_token=session["refresh_token"], access_token=session["access_token"], ) @router.get("/workflow-runs") async def get_workflow_runs( page: int = Query(1, ge=1, description="Page number (starts from 1)"), limit: int = Query(50, ge=1, le=100, description="Number of items per page"), filters: Optional[str] = Query(None, description="JSON-encoded filter criteria"), sort_by: Optional[str] = Query( None, description="Field to sort by (e.g., 'duration', 'created_at')" ), sort_order: Optional[str] = Query( "desc", description="Sort order ('asc' or 'desc')" ), user: UserModel = Depends(get_superuser), ) -> SuperuserWorkflowRunsListResponse: """ Get paginated list of all workflow runs with organization information. Requires superuser privileges. Filters should be provided as a JSON-encoded array of filter criteria. Example: [{"field": "id", "type": "number", "value": {"value": 680}}] """ offset = (page - 1) * limit # Parse filters if provided filter_criteria = None if filters: try: filter_criteria = json.loads(filters) except json.JSONDecodeError: raise HTTPException(status_code=400, detail="Invalid filter format") # Validate sort_order if sort_order not in ("asc", "desc"): sort_order = "desc" workflow_runs, total_count = await db_client.get_workflow_runs_for_superadmin( limit=limit, offset=offset, filters=filter_criteria, sort_by=sort_by, sort_order=sort_order, ) total_pages = (total_count + limit - 1) // limit # Ceiling division return SuperuserWorkflowRunsListResponse( workflow_runs=[SuperuserWorkflowRunResponse(**run) for run in workflow_runs], total_count=total_count, page=page, limit=limit, total_pages=total_pages, ) # ------------------ Admin Comment ------------------ class AdminCommentRequest(BaseModel): admin_comment: str class AdminCommentResponse(BaseModel): success: bool admin_comment: str admin_comment_ts: datetime # ------------------ Routes ------------------ @router.post("/workflow-runs/{run_id}/comment", response_model=AdminCommentResponse) async def set_admin_comment( run_id: int, request: AdminCommentRequest, user: UserModel = Depends(get_superuser), ): """Add or update an *admin-only* comment for a workflow run. The comment is stored inside the ``annotations`` JSON column under the ``admin_comment`` key so that it does not interfere with any other annotations recorded by the system. """ await db_client.update_admin_comment( run_id=run_id, admin_comment=request.admin_comment ) # Fetch the updated run to get the timestamp from annotations updated_run = await db_client.get_workflow_run_by_id(run_id) admin_comment_ts = None if updated_run and updated_run.annotations: admin_comment_ts = updated_run.annotations.get("admin_comment_ts") return AdminCommentResponse( success=True, admin_comment=request.admin_comment, admin_comment_ts=admin_comment_ts, )