feat(5-03-02): create admin export endpoints for inventory snapshot and audit trail with authorization
This commit is contained in:
@@ -9,7 +9,7 @@ from slowapi.util import get_remote_address
|
|||||||
from . import models
|
from . import models
|
||||||
from .database import engine
|
from .database import engine
|
||||||
from .routers import items, operations, users, auth, sync, categories
|
from .routers import items, operations, users, auth, sync, categories
|
||||||
from .routers.admin import backups, ai_config, db_config
|
from .routers.admin import backups, ai_config, db_config, exports
|
||||||
from .logger import log
|
from .logger import log
|
||||||
from .scheduler import scheduler, sync_scheduler_config
|
from .scheduler import scheduler, sync_scheduler_config
|
||||||
from .services.image_storage import ensure_image_directories, IMAGES_ROOT
|
from .services.image_storage import ensure_image_directories, IMAGES_ROOT
|
||||||
@@ -173,6 +173,7 @@ app.include_router(categories.router)
|
|||||||
app.include_router(backups.router)
|
app.include_router(backups.router)
|
||||||
app.include_router(ai_config.router)
|
app.include_router(ai_config.router)
|
||||||
app.include_router(db_config.router)
|
app.include_router(db_config.router)
|
||||||
|
app.include_router(exports.router)
|
||||||
|
|
||||||
# [STATIC FILES] Mount /images/ directory for serving uploaded photos
|
# [STATIC FILES] Mount /images/ directory for serving uploaded photos
|
||||||
# Ensure directory exists before mounting (StaticFiles requires pre-existing directory)
|
# Ensure directory exists before mounting (StaticFiles requires pre-existing directory)
|
||||||
|
|||||||
141
backend/routers/admin/exports.py
Normal file
141
backend/routers/admin/exports.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
"""
|
||||||
|
Admin export endpoints for inventory snapshot and audit trail exports.
|
||||||
|
Supports CSV and Excel formats.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from backend.database import get_db
|
||||||
|
from backend.auth import get_current_admin
|
||||||
|
from backend.models import Item, AuditLog
|
||||||
|
from backend.services.export_service import (
|
||||||
|
InventorySnapshotExporter,
|
||||||
|
AuditTrailExporter,
|
||||||
|
get_export_filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/admin/exports", tags=["admin-exports"])
|
||||||
|
|
||||||
|
|
||||||
|
def validate_export_format(format_str: str) -> str:
|
||||||
|
"""Validate and normalize export format."""
|
||||||
|
format_lower = format_str.lower() if format_str else "csv"
|
||||||
|
if format_lower not in ("csv", "xlsx"):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Invalid format. Use 'csv' or 'xlsx'."
|
||||||
|
)
|
||||||
|
return format_lower
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/inventory-snapshot")
|
||||||
|
async def export_inventory_snapshot(
|
||||||
|
format: str = Query("csv", description="Export format: csv or xlsx"),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
admin_user=Depends(get_current_admin),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Export inventory snapshot in CSV or Excel format.
|
||||||
|
|
||||||
|
Requires admin authorization.
|
||||||
|
Returns file download with timestamp in filename.
|
||||||
|
"""
|
||||||
|
format_type = validate_export_format(format)
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# Fetch all items
|
||||||
|
items = db.query(Item).all()
|
||||||
|
|
||||||
|
# Generate export
|
||||||
|
if format_type == "csv":
|
||||||
|
content = InventorySnapshotExporter.to_csv(items, timestamp)
|
||||||
|
media_type = "text/csv; charset=utf-8"
|
||||||
|
else:
|
||||||
|
content = InventorySnapshotExporter.to_excel(items, timestamp)
|
||||||
|
media_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
|
||||||
|
filename = get_export_filename("inventory_snapshot", format_type, timestamp)
|
||||||
|
|
||||||
|
# Log export action
|
||||||
|
from backend.models import AuditLog
|
||||||
|
audit_entry = AuditLog(
|
||||||
|
user_id=admin_user.id,
|
||||||
|
action="EXPORT_INVENTORY_SNAPSHOT",
|
||||||
|
details=f"Exported in {format_type} format",
|
||||||
|
)
|
||||||
|
db.add(audit_entry)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Return file response
|
||||||
|
if format_type == "csv":
|
||||||
|
# For CSV, use FileResponse with bytes
|
||||||
|
import io
|
||||||
|
return FileResponse(
|
||||||
|
io.BytesIO(content.encode("utf-8")),
|
||||||
|
media_type=media_type,
|
||||||
|
filename=filename,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# For Excel, content is already bytes
|
||||||
|
import io
|
||||||
|
return FileResponse(
|
||||||
|
io.BytesIO(content),
|
||||||
|
media_type=media_type,
|
||||||
|
filename=filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/audit-trail")
|
||||||
|
async def export_audit_trail(
|
||||||
|
format: str = Query("csv", description="Export format: csv or xlsx"),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
admin_user=Depends(get_current_admin),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Export audit trail in CSV or Excel format.
|
||||||
|
|
||||||
|
Requires admin authorization.
|
||||||
|
Returns file download with timestamp in filename.
|
||||||
|
"""
|
||||||
|
format_type = validate_export_format(format)
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# Fetch all audit logs
|
||||||
|
logs = db.query(AuditLog).all()
|
||||||
|
|
||||||
|
# Generate export
|
||||||
|
if format_type == "csv":
|
||||||
|
content = AuditTrailExporter.to_csv(logs, timestamp)
|
||||||
|
media_type = "text/csv; charset=utf-8"
|
||||||
|
else:
|
||||||
|
content = AuditTrailExporter.to_excel(logs, timestamp)
|
||||||
|
media_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
|
||||||
|
filename = get_export_filename("audit_trail", format_type, timestamp)
|
||||||
|
|
||||||
|
# Log export action
|
||||||
|
audit_entry = AuditLog(
|
||||||
|
user_id=admin_user.id,
|
||||||
|
action="EXPORT_AUDIT_TRAIL",
|
||||||
|
details=f"Exported in {format_type} format",
|
||||||
|
)
|
||||||
|
db.add(audit_entry)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Return file response
|
||||||
|
if format_type == "csv":
|
||||||
|
import io
|
||||||
|
return FileResponse(
|
||||||
|
io.BytesIO(content.encode("utf-8")),
|
||||||
|
media_type=media_type,
|
||||||
|
filename=filename,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
import io
|
||||||
|
return FileResponse(
|
||||||
|
io.BytesIO(content),
|
||||||
|
media_type=media_type,
|
||||||
|
filename=filename,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user