dograh/api/routes/superuser.py

186 lines
5.7 KiB
Python
Raw Normal View History

2025-09-09 14:37:32 +05:30
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"),
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")
workflow_runs, total_count = await db_client.get_workflow_runs_for_superadmin(
limit=limit, offset=offset, filters=filter_criteria
)
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,
)