127 lines
3.8 KiB
Python
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()
|