Files
app-service/backend/app/api/v1/api_keys.py
matteoscrugli 8c4a555b88 Add comprehensive backend features and mobile UI improvements
Backend:
- Add 2FA authentication with TOTP support
- Add API keys management system
- Add audit logging for security events
- Add file upload/management system
- Add notifications system with preferences
- Add session management
- Add webhooks integration
- Add analytics endpoints
- Add export functionality
- Add password policy enforcement
- Add new database migrations for core tables

Frontend:
- Add module position system (top/bottom sidebar sections)
- Add search and notifications module configuration tabs
- Add mobile logo replacing hamburger menu
- Center page title absolutely when no tabs present
- Align sidebar footer toggles with navigation items
- Add lighter icon color in dark theme for mobile
- Add API keys management page
- Add notifications page with context
- Add admin analytics and audit logs pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 22:27:32 +01:00

247 lines
6.7 KiB
Python

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