"""API Key management endpoints.""" import json from typing import Any from fastapi import APIRouter, Depends, HTTPException, status, Request from sqlalchemy.orm import Session from app.dependencies import get_db, get_current_user from app.models.user import User from app import crud from app.schemas.api_key import ( APIKey, APIKeyCreate, APIKeyUpdate, APIKeyWithSecret, APIKeyList ) router = APIRouter() MAX_KEYS_PER_USER = 10 # Limit API keys per user def get_client_ip(request: Request) -> str: """Extract client IP from request.""" forwarded = request.headers.get("X-Forwarded-For") if forwarded: return forwarded.split(",")[0].strip() return request.client.host if request.client else "unknown" def serialize_api_key(db_obj) -> dict: """Serialize API key for response.""" scopes = None if db_obj.scopes: try: scopes = json.loads(db_obj.scopes) except json.JSONDecodeError: scopes = [] return { "id": db_obj.id, "user_id": db_obj.user_id, "name": db_obj.name, "key_prefix": db_obj.key_prefix, "scopes": scopes, "is_active": db_obj.is_active, "last_used_at": db_obj.last_used_at, "last_used_ip": db_obj.last_used_ip, "usage_count": int(db_obj.usage_count or "0"), "expires_at": db_obj.expires_at, "created_at": db_obj.created_at, "updated_at": db_obj.updated_at } @router.post("", response_model=APIKeyWithSecret, status_code=status.HTTP_201_CREATED) def create_api_key( request: Request, *, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), key_in: APIKeyCreate ) -> Any: """ Create a new API key. The actual key is only returned once on creation. """ # Check key limit key_count = crud.api_key.count_by_user(db, current_user.id) if key_count >= MAX_KEYS_PER_USER: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Maximum {MAX_KEYS_PER_USER} API keys allowed per user" ) db_obj, plain_key = crud.api_key.create(db, obj_in=key_in, user_id=current_user.id) # Log the action crud.audit_log.log_action( db, user_id=current_user.id, username=current_user.username, action="create", resource_type="api_key", resource_id=db_obj.id, details={"name": key_in.name}, ip_address=get_client_ip(request), user_agent=request.headers.get("User-Agent", "")[:500], status="success" ) result = serialize_api_key(db_obj) result["key"] = plain_key return result @router.get("", response_model=APIKeyList) def list_api_keys( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ) -> Any: """ List all API keys for the current user. """ keys = crud.api_key.get_multi_by_user(db, user_id=current_user.id) total = crud.api_key.count_by_user(db, current_user.id) return { "items": [serialize_api_key(k) for k in keys], "total": total } @router.get("/{key_id}", response_model=APIKey) def get_api_key( key_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ) -> Any: """ Get a specific API key. """ db_obj = crud.api_key.get(db, id=key_id) if not db_obj or db_obj.user_id != current_user.id: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="API key not found" ) return serialize_api_key(db_obj) @router.patch("/{key_id}", response_model=APIKey) def update_api_key( request: Request, key_id: str, *, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), key_in: APIKeyUpdate ) -> Any: """ Update an API key. """ db_obj = crud.api_key.get(db, id=key_id) if not db_obj or db_obj.user_id != current_user.id: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="API key not found" ) db_obj = crud.api_key.update(db, db_obj=db_obj, obj_in=key_in) # Log the action crud.audit_log.log_action( db, user_id=current_user.id, username=current_user.username, action="update", resource_type="api_key", resource_id=db_obj.id, details={"name": db_obj.name, "changes": key_in.model_dump(exclude_unset=True)}, ip_address=get_client_ip(request), user_agent=request.headers.get("User-Agent", "")[:500], status="success" ) return serialize_api_key(db_obj) @router.post("/{key_id}/revoke", response_model=APIKey) def revoke_api_key( request: Request, key_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ) -> Any: """ Revoke (deactivate) an API key. """ db_obj = crud.api_key.get(db, id=key_id) if not db_obj or db_obj.user_id != current_user.id: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="API key not found" ) db_obj = crud.api_key.revoke(db, id=key_id) # Log the action crud.audit_log.log_action( db, user_id=current_user.id, username=current_user.username, action="revoke", resource_type="api_key", resource_id=db_obj.id, details={"name": db_obj.name}, ip_address=get_client_ip(request), user_agent=request.headers.get("User-Agent", "")[:500], status="success" ) return serialize_api_key(db_obj) @router.delete("/{key_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_api_key( request: Request, key_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ) -> None: """ Delete an API key. """ db_obj = crud.api_key.get(db, id=key_id) if not db_obj or db_obj.user_id != current_user.id: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="API key not found" ) key_name = db_obj.name if not crud.api_key.delete(db, id=key_id): raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to delete API key" ) # Log the action crud.audit_log.log_action( db, user_id=current_user.id, username=current_user.username, action="delete", resource_type="api_key", resource_id=key_id, details={"name": key_name}, ip_address=get_client_ip(request), user_agent=request.headers.get("User-Agent", "")[:500], status="success" )