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>
371 lines
10 KiB
Python
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
|