urmi-ai/backend/app/routes_api_keys.py
2026-10-02 11:22:40 +05:30

127 lines
3.8 KiB
Python

from datetime import datetime, timezone
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.orm import Session
from .api_keys_service import generate_api_token, hash_api_token, token_prefix, validate_key_name
from .auth import current_user
from .database import get_db
from .models import ApiKey, UserAccount
router = APIRouter(prefix="/api/v1/api-keys", tags=["api-keys"])
class CreateApiKeyRequest(BaseModel):
name: str = Field(min_length=2, max_length=60)
def serialize_api_key(api_key: ApiKey, *, include_token: str | None = None) -> dict:
payload = {
"id": str(api_key.id),
"name": api_key.name,
"key_prefix": api_key.key_prefix,
"created_at": api_key.created_at,
"last_used_at": api_key.last_used_at,
"revoked_at": api_key.revoked_at,
"status": "revoked" if api_key.revoked_at else "active",
}
if include_token:
payload["token"] = include_token
return payload
@router.get("")
def list_api_keys(
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> list[dict]:
keys = db.scalars(
select(ApiKey)
.where(ApiKey.user_id == user.id, ApiKey.revoked_at.is_(None))
.order_by(ApiKey.created_at.desc())
).all()
return [serialize_api_key(key) for key in keys]
@router.post("", status_code=status.HTTP_201_CREATED)
def create_api_key(
payload: CreateApiKeyRequest,
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> dict:
try:
name = validate_key_name(payload.name)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
existing = db.scalar(
select(ApiKey).where(
ApiKey.user_id == user.id,
ApiKey.name == name,
ApiKey.revoked_at.is_(None),
)
)
if existing:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="An active API key with this name already exists")
api_key = ApiKey(
user_id=user.id,
name=name,
key_prefix="pending",
key_hash="pending",
)
db.add(api_key)
db.flush()
token = generate_api_token(api_key.id)
api_key.key_prefix = token_prefix(token)
api_key.key_hash = hash_api_token(token)
db.commit()
db.refresh(api_key)
return serialize_api_key(api_key, include_token=token)
@router.post("/{key_id}/regenerate")
def regenerate_api_key(
key_id: UUID,
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> dict:
api_key = db.scalar(
select(ApiKey).where(
ApiKey.id == key_id,
ApiKey.user_id == user.id,
ApiKey.revoked_at.is_(None),
)
)
if not api_key:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="API key not found")
token = generate_api_token(api_key.id)
api_key.key_prefix = token_prefix(token)
api_key.key_hash = hash_api_token(token)
api_key.last_used_at = None
db.commit()
db.refresh(api_key)
return serialize_api_key(api_key, include_token=token)
@router.delete("/{key_id}", status_code=status.HTTP_204_NO_CONTENT)
def revoke_api_key(
key_id: UUID,
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> None:
api_key = db.scalar(
select(ApiKey).where(
ApiKey.id == key_id,
ApiKey.user_id == user.id,
ApiKey.revoked_at.is_(None),
)
)
if not api_key:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="API key not found")
api_key.revoked_at = datetime.now(timezone.utc)
db.commit()