Files
app-service/backend/app/api/v1/export.py
matteoscrugli fc605f03c9 Improve file listing and fix notification metadata field
Backend:
- Optimize file listing for non-superusers with dedicated CRUD methods
- Add get_visible_for_user and count_visible_for_user for efficient queries
- Move /allowed-types/ and /max-size/ routes before /{file_id} for proper matching
- Rename notification 'metadata' field to 'extra_data' for clarity
- Fix settings export to use get_value() method

Frontend:
- Update NotificationItem interface to use extra_data field

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

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

371 lines
10 KiB
Python

"""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.get_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