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:
2025-12-17 22:27:32 +01:00
parent f698aa4d51
commit 8c4a555b88
76 changed files with 9751 additions and 323 deletions

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