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>
This commit is contained in:
246
backend/app/api/v1/api_keys.py
Normal file
246
backend/app/api/v1/api_keys.py
Normal file
@@ -0,0 +1,246 @@
|
||||
"""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"
|
||||
)
|
||||
Reference in New Issue
Block a user