diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py index 184b3b8..9aeb3f4 100755 --- a/backend/app/api/routes.py +++ b/backend/app/api/routes.py @@ -3,6 +3,7 @@ import uuid from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, Form +from fastapi.responses import Response from sqlalchemy.ext.asyncio import AsyncSession from app.api.schemas import ( @@ -21,6 +22,7 @@ from app.api.schemas import ( DropdownOptionsResponse, AgencyResponse, UserResponse, + SupportEmailRequest, ) from app.dependencies.auth import get_current_user from app.models.database import get_db @@ -32,6 +34,7 @@ from app.repositories import ( DropdownRepository, ) from app.services.storage_service import storage_service +from app.services.email_service import email_service router = APIRouter() @@ -640,3 +643,49 @@ async def list_agencies( agencies = result.scalars().all() return [AgencyResponse(id=a.id, name=a.name) for a in agencies] + + +# File download endpoint +@router.get("/files/{storage_key:path}") +async def get_file( + storage_key: str, + db: AsyncSession = Depends(get_db), + user: dict = Depends(get_current_user), +): + """Retrieve a stored file by its storage key.""" + file_data = await storage_service.get_file(storage_key) + if file_data is None: + raise HTTPException(status_code=404, detail="File not found") + + # Determine content type from extension + extension = storage_key.split('.')[-1].lower() if '.' in storage_key else '' + content_types = { + 'png': 'image/png', + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'gif': 'image/gif', + 'webp': 'image/webp', + 'svg': 'image/svg+xml', + 'pdf': 'application/pdf', + } + return Response( + content=file_data, + media_type=content_types.get(extension, 'application/octet-stream'), + ) + + +# Support email endpoint (public - no auth required for login page access) +@router.post("/support/email") +async def send_support_email( + data: SupportEmailRequest, +): + """Send support email - no auth required (for login page).""" + success = await email_service.send_support_email( + message=data.message, + subject=data.subject, + user_name=data.user_name, + user_email=data.user_email, + ) + if not success: + raise HTTPException(status_code=500, detail="Failed to send email") + return {"success": True, "message": "Email sent successfully"} diff --git a/frontend/App.tsx b/frontend/App.tsx index 57e960a..8862eaa 100755 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -286,8 +286,8 @@ const App: React.FC = () => { const handleRetryAnalysis = async (campaignName: string, tempId: string) => { const proofToRetry = campaignProofs[campaignName]?.find(proof => proof.tempId === tempId); - if (!proofToRetry || !proofToRetry.file) { - console.error("Proof to retry not found or file is missing"); + if (!proofToRetry) { + console.error("Proof to retry not found"); return; } @@ -298,7 +298,25 @@ const App: React.FC = () => { return; } - const { file, proofName, channel, subChannel, proofType } = proofToRetry; + let file = proofToRetry.file; + + // Fetch from backend if not in memory + if (!file) { + const storageKey = proofToRetry.fileStorageKey || proofToRetry.versions?.[0]?.fileStorageKey; + if (!storageKey) { + setError('Cannot retry: original file is not available.'); + return; + } + try { + file = await apiService.getFile(storageKey); + } catch (err) { + console.error("Failed to fetch file for retry:", err); + setError('Failed to retrieve original file for retry.'); + return; + } + } + + const { proofName, channel, subChannel, proofType } = proofToRetry; setCampaignProofs(prevProofs => ({ ...prevProofs, diff --git a/frontend/components/Campaigns.tsx b/frontend/components/Campaigns.tsx index 625c683..560817a 100755 --- a/frontend/components/Campaigns.tsx +++ b/frontend/components/Campaigns.tsx @@ -24,6 +24,7 @@ import { PDFIcon } from './icons/PDFIcon'; import { PDFReport } from './PDFReport'; import { ExportIcon } from './icons/ExportIcon'; import { XIcon } from './icons/XIcon'; +import apiService from '../services/apiService'; export const initialCampaigns = [ @@ -1110,10 +1111,27 @@ const ProofDetailView: React.FC<{ }; const handleDownload = async () => { - const url = selectedVersion.proofPreviewUrl; const fileName = `${proof.proofName}_V${selectedVersion.version}`; + const storageKey = selectedVersion.fileStorageKey; try { + // Prefer fetching the original file from backend storage + if (storageKey) { + const file = await apiService.getFile(storageKey); + const blobUrl = URL.createObjectURL(file); + const link = document.createElement('a'); + link.href = blobUrl; + const ext = storageKey.split('.').pop() || 'png'; + link.download = `${fileName}.${ext}`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(blobUrl); + return; + } + + // Fallback to preview URL if no storage key (legacy data) + const url = selectedVersion.proofPreviewUrl; if (url.startsWith('data:')) { // Data URL - extract mime type and download directly const link = document.createElement('a'); diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index 4318924..a87dd5b 100755 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -182,6 +182,17 @@ class ApiService { return this.fetch(`/proofs/${id}`); } + async getFile(storageKey: string): Promise { + const headers = await this.getHeaders(); + const response = await fetch(`${API_URL}/api/files/${encodeURIComponent(storageKey)}`, { headers }); + if (!response.ok) { + throw new Error(`Failed to fetch file: HTTP ${response.status}`); + } + const blob = await response.blob(); + const filename = storageKey.split('/').pop() || 'proof'; + return new File([blob], filename, { type: blob.type }); + } + async deleteProof(id: string): Promise { return this.fetch(`/proofs/${id}`, { method: 'DELETE', @@ -265,8 +276,10 @@ class ApiService { proofPreviewUrl: v.thumbnail_url || '', feedback: v.agent_review || {} as AgentReview, overallStatus: v.overall_status as any, + fileStorageKey: v.file_storage_key || '', })), _id: proof.id, + fileStorageKey: latestVersion?.file_storage_key || '', }; } @@ -361,6 +374,19 @@ class ApiService { async getAgencies(): Promise { return this.fetch('/agencies'); } + + // Support email endpoint + async sendSupportEmail(data: { + message: string; + subject: string; + user_name?: string; + user_email?: string; + }): Promise<{ success: boolean; message: string }> { + return this.fetch('/support/email', { + method: 'POST', + body: JSON.stringify(data), + }); + } } export interface DropdownOptionsResponse {