"""Tenant API router - tenant info, usage, and auth bootstrap."""
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
import jwt
from app.dependencies import get_kumiho_client, get_user_token, ensure_tenant_context
from typing import Any
import kumiho
router = APIRouter()
[docs]
class UserInfo(BaseModel):
"""User information response."""
email: str
id: str
[docs]
class BootstrapResponse(BaseModel):
"""Session bootstrap response for frontend initialization."""
tenant_id: str
project_names: List[str]
anonymous_allowed: bool
[docs]
class TenantUsageResponse(BaseModel):
"""Tenant usage and limits."""
tenant_id: str
node_count: int
node_limit: int
[docs]
@router.get("/bootstrap", response_model=BootstrapResponse)
async def bootstrap(
client: Any = Depends(get_kumiho_client)
):
"""
Return public tenant configuration for frontend bootstrapping.
This replaces the need for NEXT_PUBLIC_TENANT_ID and other build-time secrets.
The frontend can call this endpoint to discover tenant configuration dynamically.
"""
try:
context = ensure_tenant_context()
projects = kumiho.get_projects()
project_names = [p.name for p in projects]
return BootstrapResponse(
tenant_id=context.tenant_id,
project_names=project_names,
anonymous_allowed=True # Can be made configurable via tenant settings
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to bootstrap session: {str(e)}")
[docs]
@router.get("/whoami", response_model=UserInfo)
async def whoami(
project_name: Optional[str] = Query(None, description="Project name to validate membership against"),
client: Any = Depends(get_kumiho_client),
token: Optional[str] = Depends(get_user_token)
):
"""
Return information about the authenticated user.
Optionally validates membership in a specific project.
"""
try:
if not token:
return UserInfo(email="anonymous", id="anonymous")
# Validate against Kumiho Backend (Tenant Membership)
if project_name:
try:
kumiho.get_project(project_name)
except Exception as e:
raise HTTPException(status_code=401, detail=f"User not authorized: {str(e)}")
decoded = jwt.decode(token, options={"verify_signature": False})
email = decoded.get('email') or decoded.get('sub') or "Unknown"
user_id = decoded.get('sub') or "Unknown"
return UserInfo(email=email, id=user_id)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=401, detail="Invalid authentication")
[docs]
@router.get("/usage", response_model=TenantUsageResponse)
async def get_tenant_usage(
client: Any = Depends(get_kumiho_client)
):
"""Get the current tenant's usage statistics and limits."""
try:
usage = client.get_tenant_usage()
return TenantUsageResponse(
tenant_id=usage.get("tenant_id", ""),
node_count=usage.get("node_count", 0),
node_limit=usage.get("node_limit", 0)
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))