"""API Token routes — create, list, revoke Bearer tokens for programmatic access.""" from __future__ import annotations import uuid from datetime import UTC, datetime, timedelta from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from app.core.api_token import create_api_token, list_api_tokens, revoke_api_token from app.core.db import get_db from app.deps import require_permission router = APIRouter(prefix="/api/v1/tokens", tags=["api-tokens"]) class TokenCreateRequest(BaseModel): name: str scopes: list[str] = [] expires_in_days: int | None = None class TokenRevokeRequest(BaseModel): token_id: str @router.post("", status_code=status.HTTP_201_CREATED) async def create_token( body: TokenCreateRequest, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mcp:write")), ): """Create a new API token. The plaintext token is returned ONCE.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) expires_at = None if body.expires_in_days is not None: expires_at = datetime.now(UTC) + timedelta(days=body.expires_in_days) result = await create_api_token( db, tenant_id, user_id, body.name, body.scopes, expires_at, ) await db.commit() return result @router.get("") async def list_tokens( db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mcp:read")), ): """List all API tokens for the current user (without token hashes).""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) tokens = await list_api_tokens(db, tenant_id, user_id) return {"items": tokens, "total": len(tokens)} @router.delete("/{token_id}", status_code=status.HTTP_204_NO_CONTENT) async def revoke_token( token_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mcp:write")), ): """Revoke an API token.""" tenant_id = uuid.UUID(current_user["tenant_id"]) try: tid = uuid.UUID(token_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid token_id", "code": "invalid_id"}) from None revoked = await revoke_api_token(db, tenant_id, tid) if not revoked: raise HTTPException(404, detail={"detail": "Token not found or already revoked", "code": "not_found"}) await db.commit()