dograh/api/routes/superuser.py
2026-07-11 15:51:36 +05:30

204 lines
6.8 KiB
Python

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 (
StackAuthSessionError,
StackAuthUserSearchError,
stackauth,
)
router = APIRouter(prefix="/superuser", tags=["superuser"])
class ImpersonateRequest(BaseModel):
"""Request payload for superadmin impersonation.
``provider_user_id``, ``user_id``, or ``email`` may be supplied. If more
than one is provided, ``provider_user_id`` takes precedence, followed by
``user_id`` and then ``email``.
"""
provider_user_id: str | None = None
user_id: int | None = None
email: str | 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]
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 = (
request.provider_user_id.strip() if request.provider_user_id else None
) or None
email = request.email.strip().lower() if request.email else None
# ------------------------------------------------------------------
# Fallback: resolve provider_user_id from internal ``user_id`` or email.
# ------------------------------------------------------------------
if provider_user_id is None:
if request.user_id is not None:
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
elif email:
db_user = await db_client.get_user_by_email(email)
if db_user is not None:
provider_user_id = db_user.provider_id
else:
try:
stack_users = await stackauth.find_users_by_email(email)
except StackAuthUserSearchError as exc:
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="Failed to search Stack Auth users.",
) from exc
if len(stack_users) == 1 and isinstance(stack_users[0].get("id"), str):
provider_user_id = stack_users[0]["id"]
elif len(stack_users) > 1:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Multiple Stack Auth users matched that email.",
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with email {email} not found.",
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=(
"One of 'provider_user_id', 'user_id', or 'email' must be provided."
),
)
# ------------------------------------------------------------------
# Call Stack Auth to create the impersonation session
# ------------------------------------------------------------------
try:
session = await stackauth.impersonate(provider_user_id)
except StackAuthSessionError as exc:
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="Failed to create Stack Auth impersonation session.",
) from exc
if (
not isinstance(session, dict)
or "refresh_token" not in session
or "access_token" not in session
):
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="Failed to create Stack Auth impersonation session.",
)
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,
)