first commit
This commit is contained in:
commit
efa5080897
127 changed files with 18610 additions and 0 deletions
127
backend/app/routes_api_keys.py
Normal file
127
backend/app/routes_api_keys.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue