- Fix cost tracker dashboard URL (cost.oliver.agency → optical-dev.oliver.solutions/cost-tracker/analytics) in UserList, QCDetail, FinalDetail; centralise into src/lib/costTracker.ts - Wire audit logging across backend routes (was 1 call site, now covers all key events): · routes_auth: LOGIN_SUCCESS/FAILURE for local + MS SSO, LOGOUT · routes_files: FILE_UPLOAD on signed URL generation · routes_jobs: JOB_CREATE, JOB_APPROVE, JOB_REJECT, JOB_STATUS_CHANGE, JOB_DELETE, VTT_EDIT · routes_admin: USER_CREATE, USER_UPDATE, USER_ROLE_CHANGE, USER_DEACTIVATE - Add Audit Log admin UI page (/admin/audit-log): · Three tabs: All Events (paginated, server-side filters), Security Events, User Activity · Filters: action group, severity, success/failure, free-text search · Click-to-expand row shows IP, request ID, resource, details JSON · Wired into App.tsx (RoleGate: production + admin) and sidebar nav Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
65 lines
No EOL
2.3 KiB
Python
65 lines
No EOL
2.3 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
from motor.motor_asyncio import AsyncIOMotorDatabase
|
|
|
|
from ...core.database import get_database
|
|
from ...core.dependencies import get_current_user
|
|
from ...models.user import User
|
|
from ...schemas.file import SignedUploadRequest, SignedUploadResponse
|
|
from ...services.gcs import generate_signed_upload_url
|
|
from ...services.audit_logger import audit_logger
|
|
from ...models.audit_log import AuditAction
|
|
|
|
router = APIRouter(prefix="/files", tags=["files"])
|
|
|
|
|
|
@router.post("/signed-upload", response_model=SignedUploadResponse)
|
|
async def get_signed_upload_url(
|
|
request: SignedUploadRequest,
|
|
http_request: Request,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncIOMotorDatabase = Depends(get_database),
|
|
):
|
|
"""
|
|
Generate a signed URL for direct browser-to-GCS upload
|
|
This optimizes large file uploads by bypassing the API server
|
|
"""
|
|
if not request.content_type.startswith("video/"):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Only video files are supported"
|
|
)
|
|
|
|
# Generate unique blob path
|
|
from bson import ObjectId
|
|
blob_path = f"temp/{ObjectId()}/{request.filename}"
|
|
|
|
try:
|
|
# Generate signed upload URL with form fields
|
|
signed_data = await generate_signed_upload_url(
|
|
blob_path=blob_path,
|
|
content_type=request.content_type,
|
|
max_size=request.max_size or 1024 * 1024 * 1024 # 1GB default
|
|
)
|
|
|
|
await audit_logger.log_action(
|
|
action=AuditAction.FILE_UPLOAD,
|
|
description=f"Signed upload URL generated for {request.filename}",
|
|
user=current_user,
|
|
request=http_request,
|
|
resource_type="file",
|
|
resource_name=request.filename,
|
|
details={"blob_path": blob_path, "content_type": request.content_type},
|
|
)
|
|
return SignedUploadResponse(
|
|
upload_url=signed_data["url"],
|
|
fields=signed_data["fields"],
|
|
blob_path=blob_path
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to generate signed upload URL: {str(e)}"
|
|
) |