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()