amazon-transcreation/backend/app/api/v1/files.py
DJP 5ef7e588b6 feat: wire analytics to real data and add audit logging across all endpoints
Replace mock chart data on reports page with real backend queries (jobs over
time, locale stats, usage stats, quality metrics). Add audit logging to auth
(login/login_failed), file management (upload/delete TM and reference files),
and feedback submission so the system logs page shows complete activity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 17:17:14 -04:00

203 lines
6.9 KiB
Python

from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, status
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_current_user, get_db
from app.models.files import ReferenceFileType
from app.schemas.files import (
FileUploadResponse,
ReferenceFileResponse,
TMFileResponse,
)
from app.services.audit_service import AuditService
from app.services.file_service import FileService
router = APIRouter(prefix="/files", tags=["files"])
file_service = FileService()
audit_service = AuditService()
# ---- TM Files ----
@router.post("/tm", response_model=FileUploadResponse, status_code=status.HTTP_201_CREATED)
async def upload_tm_file(
client_id: UUID = Query(...),
locale_code: str = Query(...),
channel: str = Query(...),
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> FileUploadResponse:
"""Upload a Translation Memory (JSONL) file."""
if not file.filename:
raise HTTPException(status_code=400, detail="File must have a filename")
if not file_service.validate_file_extension(file.filename, [".jsonl", ".json"]):
raise HTTPException(status_code=400, detail="Only .jsonl/.json files accepted")
tm = await file_service.upload_tm_file(
db, client_id, locale_code, channel, file.file, file.filename,
uploaded_by=current_user["user_id"],
)
await audit_service.log(
db, action="upload_tm", entity_type="tm_file", entity_id=str(tm.id),
user_id=current_user["user_id"],
details={"filename": tm.filename, "locale": locale_code, "channel": channel, "segments": tm.segment_count},
)
await db.commit()
return FileUploadResponse(
id=tm.id,
filename=tm.filename,
file_path=tm.file_path,
message=f"Uploaded TM file with {tm.segment_count} segments",
)
@router.get("/tm", response_model=list[TMFileResponse])
async def list_tm_files(
client_id: UUID = Query(...),
locale_code: str | None = Query(None),
channel: str | None = Query(None),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> list[TMFileResponse]:
"""List TM files for a client."""
files = await file_service.list_tm_files(db, client_id, locale_code, channel)
return [TMFileResponse.model_validate(f) for f in files]
@router.get("/tm/{file_id}/download")
async def download_tm_file(
file_id: UUID,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> FileResponse:
"""Download a TM file."""
from sqlalchemy import select
from app.models.files import TMFileRegistry
result = await db.execute(
select(TMFileRegistry).where(TMFileRegistry.id == file_id)
)
tm = result.scalar_one_or_none()
if tm is None:
raise HTTPException(status_code=404, detail="TM file not found")
path = file_service.get_file_path(tm.file_path)
if path is None:
raise HTTPException(status_code=404, detail="File not found on disk")
return FileResponse(path=str(path), filename=tm.filename)
@router.delete("/tm/{file_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_tm_file(
file_id: UUID,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> None:
"""Delete a TM file."""
deleted = await file_service.delete_tm_file(db, file_id)
if not deleted:
raise HTTPException(status_code=404, detail="TM file not found")
await audit_service.log(
db, action="delete_tm", entity_type="tm_file", entity_id=str(file_id),
user_id=current_user["user_id"],
)
await db.commit()
# ---- Reference Files ----
@router.post(
"/reference",
response_model=FileUploadResponse,
status_code=status.HTTP_201_CREATED,
)
async def upload_reference_file(
client_id: UUID = Query(...),
file_type: ReferenceFileType = Query(...),
locale_scope: str = Query(...),
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> FileUploadResponse:
"""Upload a reference file (glossary, blacklist, TOV, etc.)."""
if not file.filename:
raise HTTPException(status_code=400, detail="File must have a filename")
ref = await file_service.upload_reference_file(
db, client_id, file_type, locale_scope, file.file, file.filename,
uploaded_by=current_user["user_id"],
)
await audit_service.log(
db, action="upload_reference", entity_type="reference_file", entity_id=str(ref.id),
user_id=current_user["user_id"],
details={"filename": ref.filename, "file_type": file_type.value, "locale_scope": locale_scope},
)
await db.commit()
return FileUploadResponse(
id=ref.id,
filename=ref.filename,
file_path=ref.file_path,
message=f"Uploaded {file_type.value} reference file",
)
@router.get("/reference", response_model=list[ReferenceFileResponse])
async def list_reference_files(
client_id: UUID = Query(...),
file_type: ReferenceFileType | None = Query(None),
locale_scope: str | None = Query(None),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> list[ReferenceFileResponse]:
"""List reference files for a client."""
files = await file_service.list_reference_files(
db, client_id, file_type, locale_scope
)
return [ReferenceFileResponse.model_validate(f) for f in files]
@router.get("/reference/{file_id}/download")
async def download_reference_file(
file_id: UUID,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> FileResponse:
"""Download a reference file."""
from sqlalchemy import select
from app.models.files import ReferenceFile
result = await db.execute(
select(ReferenceFile).where(ReferenceFile.id == file_id)
)
ref = result.scalar_one_or_none()
if ref is None:
raise HTTPException(status_code=404, detail="Reference file not found")
path = file_service.get_file_path(ref.file_path)
if path is None:
raise HTTPException(status_code=404, detail="File not found on disk")
return FileResponse(path=str(path), filename=ref.filename)
@router.delete("/reference/{file_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_reference_file(
file_id: UUID,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> None:
"""Delete a reference file."""
deleted = await file_service.delete_reference_file(db, file_id)
if not deleted:
raise HTTPException(status_code=404, detail="Reference file not found")
await audit_service.log(
db, action="delete_reference", entity_type="reference_file", entity_id=str(file_id),
user_id=current_user["user_id"],
)
await db.commit()