Files
sriphat-dataplatform/03-apiservice/app/routes/admin_api_keys.py
jigoong 76398c3de6 feat(apiservice): add edit client/key functionality in API Management page
- PATCH /admin/api-keys/clients/{id} — update client name and is_active
- PATCH /admin/api-keys/{id} — update key name and permissions
- Edit Client modal with name field and active/inactive toggle
- Edit Key modal with name field and permissions JSON textarea (pre-filled)
- Fix JS syntax error: use data-* attributes instead of inline JSON in onclick
2026-06-09 00:41:36 +07:00

197 lines
6.2 KiB
Python

"""
API Client and API Key management endpoints (Admin only)
Uses Keycloak admin role authentication — same pattern as admin_users.py
"""
from typing import List
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from pydantic import BaseModel
from datetime import datetime
from app.db.session import get_db
from app.db.models import ApiClient, ApiKey
from app.security.permissions import require_role, Roles
from app.security.api_key import generate_api_key, hash_api_key, encrypt_api_key, get_prefix
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/admin/api-keys", tags=["admin-api-keys"])
class ApiKeySchema(BaseModel):
id: int
name: str | None = None
key_prefix: str
permissions: list
is_active: bool
created_at: datetime
class Config:
from_attributes = True
class ApiClientSchema(BaseModel):
id: int
name: str
is_active: bool
api_keys: List[ApiKeySchema] = []
class Config:
from_attributes = True
class ApiClientCreateSchema(BaseModel):
name: str
class ApiClientUpdateSchema(BaseModel):
name: str | None = None
is_active: bool | None = None
class ApiKeyCreateSchema(BaseModel):
client_id: int
name: str | None = None
permissions: list[str] = []
class ApiKeyUpdateSchema(BaseModel):
name: str | None = None
permissions: list[str] | None = None
@router.get("/clients", response_model=List[ApiClientSchema])
async def list_clients(
db: Session = Depends(get_db),
current_user: dict = Depends(require_role(Roles.ADMIN)),
):
"""List all API clients with their keys (Admin only)"""
return db.query(ApiClient).order_by(ApiClient.id).all()
@router.patch("/clients/{client_id}", response_model=ApiClientSchema)
async def update_client(
client_id: int,
data: ApiClientUpdateSchema,
db: Session = Depends(get_db),
current_user: dict = Depends(require_role(Roles.ADMIN)),
):
"""Update API client name or active status (Admin only)"""
client = db.get(ApiClient, client_id)
if not client:
raise HTTPException(status_code=404, detail="Client not found")
if data.name is not None:
existing = db.query(ApiClient).filter(ApiClient.name == data.name, ApiClient.id != client_id).first()
if existing:
raise HTTPException(status_code=400, detail="Client name already exists")
client.name = data.name
if data.is_active is not None:
client.is_active = data.is_active
db.commit()
db.refresh(client)
logger.info(f"Admin {current_user.get('username')} updated client {client_id}")
return client
@router.post("/clients", response_model=ApiClientSchema)
async def create_client(
data: ApiClientCreateSchema,
db: Session = Depends(get_db),
current_user: dict = Depends(require_role(Roles.ADMIN)),
):
"""Create a new API client (Admin only)"""
existing = db.query(ApiClient).filter(ApiClient.name == data.name).first()
if existing:
raise HTTPException(status_code=400, detail="Client name already exists")
client = ApiClient(name=data.name, is_active=True)
db.add(client)
db.commit()
db.refresh(client)
return client
@router.post("/generate")
async def generate_key(
data: ApiKeyCreateSchema,
db: Session = Depends(get_db),
current_user: dict = Depends(require_role(Roles.ADMIN)),
):
"""Generate a new API key for a client (Admin only). Returns plaintext key once."""
client = db.get(ApiClient, data.client_id)
if not client:
raise HTTPException(status_code=404, detail="Client not found")
plain_key = generate_api_key()
api_key = ApiKey(
client_id=data.client_id,
name=data.name,
key_prefix=get_prefix(plain_key),
key_hash=hash_api_key(plain_key),
encrypted_key=encrypt_api_key(plain_key),
permissions=data.permissions,
is_active=True,
)
db.add(api_key)
db.commit()
db.refresh(api_key)
logger.info(f"Admin {current_user.get('username')} created API key {api_key.id} for client {client.name}")
return {"key_id": api_key.id, "api_key": plain_key, "key_prefix": api_key.key_prefix, "permissions": data.permissions}
@router.post("/{key_id}/regenerate")
async def regenerate_key(
key_id: int,
db: Session = Depends(get_db),
current_user: dict = Depends(require_role(Roles.ADMIN)),
):
"""Regenerate an API key — preserves permissions, returns new plaintext once (Admin only)"""
api_key = db.get(ApiKey, key_id)
if not api_key:
raise HTTPException(status_code=404, detail="API Key not found")
plain_key = generate_api_key()
api_key.key_prefix = get_prefix(plain_key)
api_key.key_hash = hash_api_key(plain_key)
api_key.encrypted_key = encrypt_api_key(plain_key)
db.commit()
db.refresh(api_key)
logger.info(f"Admin {current_user.get('username')} regenerated API key {key_id}")
return {"key_id": api_key.id, "api_key": plain_key, "key_prefix": api_key.key_prefix, "permissions": api_key.permissions}
@router.patch("/{key_id}", response_model=ApiKeySchema)
async def update_key(
key_id: int,
data: ApiKeyUpdateSchema,
db: Session = Depends(get_db),
current_user: dict = Depends(require_role(Roles.ADMIN)),
):
"""Update API key name or permissions (Admin only)"""
api_key = db.get(ApiKey, key_id)
if not api_key:
raise HTTPException(status_code=404, detail="API Key not found")
if data.name is not None:
api_key.name = data.name
if data.permissions is not None:
api_key.permissions = data.permissions
db.commit()
db.refresh(api_key)
logger.info(f"Admin {current_user.get('username')} updated API key {key_id}")
return api_key
@router.patch("/{key_id}/toggle")
async def toggle_key(
key_id: int,
db: Session = Depends(get_db),
current_user: dict = Depends(require_role(Roles.ADMIN)),
):
"""Toggle API key active/inactive (Admin only)"""
api_key = db.get(ApiKey, key_id)
if not api_key:
raise HTTPException(status_code=404, detail="API Key not found")
api_key.is_active = not api_key.is_active
db.commit()
return {"key_id": key_id, "is_active": api_key.is_active}