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,370 @@
"""Data export/import endpoints."""
import csv
import io
import json
from datetime import datetime
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Response
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from pydantic import BaseModel
from app.dependencies import get_db, get_current_superuser
from app.models.user import User
from app.models.audit_log import AuditLog
from app import crud, schemas
router = APIRouter()
class ImportResult(BaseModel):
"""Import operation result."""
success: int = 0
failed: int = 0
errors: List[str] = []
# =============================================================================
# EXPORT ENDPOINTS
# =============================================================================
@router.get("/users/csv")
def export_users_csv(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_superuser),
):
"""
Export all users to CSV format.
"""
users = db.query(User).all()
# Create CSV in memory
output = io.StringIO()
writer = csv.writer(output)
# Header
writer.writerow([
"id", "username", "email", "is_active", "is_superuser",
"totp_enabled", "created_at", "last_login"
])
# Data rows
for user in users:
writer.writerow([
user.id,
user.username,
user.email,
user.is_active,
user.is_superuser,
user.totp_enabled,
user.created_at.isoformat() if user.created_at else "",
user.last_login.isoformat() if user.last_login else ""
])
output.seek(0)
# Log export
crud.audit_log.log_action(
db,
user_id=current_user.id,
username=current_user.username,
action="export",
resource_type="users",
details={"format": "csv", "count": len(users)},
status="success"
)
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv",
headers={
"Content-Disposition": f"attachment; filename=users_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.csv"
}
)
@router.get("/users/json")
def export_users_json(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_superuser),
):
"""
Export all users to JSON format.
"""
users = db.query(User).all()
data = {
"exported_at": datetime.utcnow().isoformat(),
"exported_by": current_user.username,
"count": len(users),
"users": [
{
"id": user.id,
"username": user.username,
"email": user.email,
"is_active": user.is_active,
"is_superuser": user.is_superuser,
"totp_enabled": user.totp_enabled,
"permissions": user.permissions,
"created_at": user.created_at.isoformat() if user.created_at else None,
"last_login": user.last_login.isoformat() if user.last_login else None
}
for user in users
]
}
# Log export
crud.audit_log.log_action(
db,
user_id=current_user.id,
username=current_user.username,
action="export",
resource_type="users",
details={"format": "json", "count": len(users)},
status="success"
)
content = json.dumps(data, indent=2)
return Response(
content=content,
media_type="application/json",
headers={
"Content-Disposition": f"attachment; filename=users_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.json"
}
)
@router.get("/settings/json")
def export_settings_json(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_superuser),
):
"""
Export all settings to JSON format.
"""
from app.models.settings import Settings
settings_list = db.query(Settings).all()
data = {
"exported_at": datetime.utcnow().isoformat(),
"exported_by": current_user.username,
"count": len(settings_list),
"settings": {
setting.key: setting.value
for setting in settings_list
}
}
# Log export
crud.audit_log.log_action(
db,
user_id=current_user.id,
username=current_user.username,
action="export",
resource_type="settings",
details={"format": "json", "count": len(settings_list)},
status="success"
)
content = json.dumps(data, indent=2)
return Response(
content=content,
media_type="application/json",
headers={
"Content-Disposition": f"attachment; filename=settings_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.json"
}
)
@router.get("/audit/csv")
def export_audit_csv(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_superuser),
days: int = 30
):
"""
Export audit logs to CSV format.
"""
from datetime import timedelta
since = datetime.utcnow() - timedelta(days=days)
logs = db.query(AuditLog).filter(AuditLog.created_at >= since).all()
# Create CSV in memory
output = io.StringIO()
writer = csv.writer(output)
# Header
writer.writerow([
"id", "user_id", "username", "action", "resource_type",
"resource_id", "status", "ip_address", "created_at"
])
# Data rows
for log in logs:
writer.writerow([
log.id,
log.user_id,
log.username,
log.action,
log.resource_type,
log.resource_id,
log.status,
log.ip_address,
log.created_at.isoformat() if log.created_at else ""
])
output.seek(0)
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv",
headers={
"Content-Disposition": f"attachment; filename=audit_logs_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.csv"
}
)
# =============================================================================
# IMPORT ENDPOINTS
# =============================================================================
@router.post("/users/json", response_model=ImportResult)
async def import_users_json(
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_superuser),
):
"""
Import users from JSON file.
Only creates new users, does not update existing ones.
"""
result = ImportResult()
try:
content = await file.read()
data = json.loads(content.decode())
except json.JSONDecodeError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid JSON file: {str(e)}"
)
users_data = data.get("users", [])
if not users_data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No users found in file"
)
for user_data in users_data:
try:
username = user_data.get("username")
email = user_data.get("email")
# Check if user already exists
if crud.user.get_by_username(db, username=username):
result.errors.append(f"User '{username}' already exists")
result.failed += 1
continue
if crud.user.get_by_email(db, email=email):
result.errors.append(f"Email '{email}' already exists")
result.failed += 1
continue
# Create user with a default password (must be changed)
import secrets
temp_password = secrets.token_urlsafe(16)
user_create = schemas.UserCreate(
username=username,
email=email,
password=temp_password,
is_active=user_data.get("is_active", True),
is_superuser=user_data.get("is_superuser", False),
permissions=user_data.get("permissions")
)
crud.user.create(db, obj_in=user_create)
result.success += 1
except Exception as e:
result.errors.append(f"Error importing user: {str(e)}")
result.failed += 1
# Log import
crud.audit_log.log_action(
db,
user_id=current_user.id,
username=current_user.username,
action="import",
resource_type="users",
details={
"format": "json",
"success": result.success,
"failed": result.failed
},
status="success" if result.failed == 0 else "partial"
)
return result
@router.post("/settings/json", response_model=ImportResult)
async def import_settings_json(
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_superuser),
):
"""
Import settings from JSON file.
Updates existing settings and creates new ones.
"""
result = ImportResult()
try:
content = await file.read()
data = json.loads(content.decode())
except json.JSONDecodeError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid JSON file: {str(e)}"
)
settings_data = data.get("settings", {})
if not settings_data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No settings found in file"
)
for key, value in settings_data.items():
try:
crud.settings.update_setting(db, key=key, value=value)
result.success += 1
except Exception as e:
result.errors.append(f"Error importing setting '{key}': {str(e)}")
result.failed += 1
# Log import
crud.audit_log.log_action(
db,
user_id=current_user.id,
username=current_user.username,
action="import",
resource_type="settings",
details={
"format": "json",
"success": result.success,
"failed": result.failed
},
status="success" if result.failed == 0 else "partial"
)
return result