- Add GET /files/{storage_key:path} endpoint to serve stored files
- Add getFile() method to apiService to fetch files from backend
- Update convertProofToFrontend() to preserve fileStorageKey
- Update handleRetryAnalysis() to fetch file from backend when not in memory
- Update handleDownload() to download original file instead of thumbnail
After page refresh, the retry button now fetches the original file from
backend storage using the fileStorageKey, allowing failed proofs to be
reprocessed. The Download Asset button also now downloads the original
uploaded file rather than the preview thumbnail.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
691 lines
23 KiB
Python
Executable file
691 lines
23 KiB
Python
Executable file
"""REST API routes for campaigns, proofs, and audit items."""
|
|
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 (
|
|
CampaignCreate,
|
|
CampaignUpdate,
|
|
CampaignResponse,
|
|
ProofCreate,
|
|
ProofResponse,
|
|
ProofVersionResponse,
|
|
FlaggedItemCreate,
|
|
FlaggedItemResponse,
|
|
ResolvedItemCreate,
|
|
ResolvedItemResponse,
|
|
ErrorItemResponse,
|
|
AnalyticsResponse,
|
|
DropdownOptionsResponse,
|
|
AgencyResponse,
|
|
UserResponse,
|
|
SupportEmailRequest,
|
|
)
|
|
from app.dependencies.auth import get_current_user
|
|
from app.models.database import get_db
|
|
from app.repositories import (
|
|
CampaignRepository,
|
|
ProofRepository,
|
|
UserRepository,
|
|
AuditRepository,
|
|
DropdownRepository,
|
|
)
|
|
from app.services.storage_service import storage_service
|
|
from app.services.email_service import email_service
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# Helper to get user from DB based on Azure claims
|
|
async def get_db_user(
|
|
session: AsyncSession,
|
|
user_claims: dict,
|
|
) -> Optional[uuid.UUID]:
|
|
"""Get or create user from Azure AD claims and return user ID."""
|
|
user_repo = UserRepository(session)
|
|
azure_oid = user_claims.get("oid") or user_claims.get("sub")
|
|
if not azure_oid:
|
|
return None
|
|
|
|
user = await user_repo.get_or_create_from_azure(
|
|
azure_ad_oid=azure_oid,
|
|
email=user_claims.get("email", user_claims.get("preferred_username", "")),
|
|
name=user_claims.get("name", "Unknown"),
|
|
)
|
|
return user.id
|
|
|
|
|
|
# Campaign endpoints
|
|
@router.get("/campaigns", response_model=list[CampaignResponse])
|
|
async def list_campaigns(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
limit: int = Query(100, ge=1, le=500),
|
|
offset: int = Query(0, ge=0),
|
|
):
|
|
"""List all campaigns."""
|
|
repo = CampaignRepository(db)
|
|
campaigns_with_counts = await repo.get_with_proof_counts()
|
|
|
|
return [
|
|
CampaignResponse(
|
|
id=item["campaign"].id,
|
|
name=item["campaign"].name,
|
|
workfront_id=item["campaign"].workfront_id,
|
|
client_lead=item["campaign"].client_lead,
|
|
agency_lead=item["campaign"].agency_lead,
|
|
brand_guidelines=item["campaign"].brand_guidelines,
|
|
status=item["campaign"].status,
|
|
agency=item["campaign"].agency.name if item["campaign"].agency else None,
|
|
created_at=item["campaign"].created_at,
|
|
updated_at=item["campaign"].updated_at,
|
|
proofs=item["proof_count"],
|
|
)
|
|
for item in campaigns_with_counts
|
|
]
|
|
|
|
|
|
@router.post("/campaigns", response_model=CampaignResponse, status_code=201)
|
|
async def create_campaign(
|
|
data: CampaignCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Create a new campaign."""
|
|
user_id = await get_db_user(db, user)
|
|
repo = CampaignRepository(db)
|
|
|
|
# Check if campaign name already exists
|
|
existing = await repo.get_by_name(data.name)
|
|
if existing:
|
|
raise HTTPException(status_code=400, detail="Campaign with this name already exists")
|
|
|
|
campaign = await repo.create(
|
|
name=data.name,
|
|
workfront_id=data.workfront_id,
|
|
client_lead=data.client_lead,
|
|
agency_lead=data.agency_lead,
|
|
brand_guidelines=data.brand_guidelines,
|
|
created_by=user_id,
|
|
)
|
|
|
|
return CampaignResponse(
|
|
id=campaign.id,
|
|
name=campaign.name,
|
|
workfront_id=campaign.workfront_id,
|
|
client_lead=campaign.client_lead,
|
|
agency_lead=campaign.agency_lead,
|
|
brand_guidelines=campaign.brand_guidelines,
|
|
status=campaign.status,
|
|
agency=None,
|
|
created_at=campaign.created_at,
|
|
updated_at=campaign.updated_at,
|
|
proofs=0,
|
|
)
|
|
|
|
|
|
@router.get("/campaigns/{campaign_id}", response_model=CampaignResponse)
|
|
async def get_campaign(
|
|
campaign_id: uuid.UUID,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get a campaign by ID."""
|
|
repo = CampaignRepository(db)
|
|
campaign = await repo.get_by_id(campaign_id)
|
|
if not campaign:
|
|
raise HTTPException(status_code=404, detail="Campaign not found")
|
|
|
|
return CampaignResponse(
|
|
id=campaign.id,
|
|
name=campaign.name,
|
|
workfront_id=campaign.workfront_id,
|
|
client_lead=campaign.client_lead,
|
|
agency_lead=campaign.agency_lead,
|
|
brand_guidelines=campaign.brand_guidelines,
|
|
status=campaign.status,
|
|
agency=campaign.agency.name if campaign.agency else None,
|
|
created_at=campaign.created_at,
|
|
updated_at=campaign.updated_at,
|
|
proofs=len(campaign.proofs),
|
|
)
|
|
|
|
|
|
@router.put("/campaigns/{campaign_id}", response_model=CampaignResponse)
|
|
async def update_campaign(
|
|
campaign_id: uuid.UUID,
|
|
data: CampaignUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Update a campaign."""
|
|
repo = CampaignRepository(db)
|
|
campaign = await repo.update(
|
|
campaign_id,
|
|
name=data.name,
|
|
workfront_id=data.workfront_id,
|
|
client_lead=data.client_lead,
|
|
agency_lead=data.agency_lead,
|
|
brand_guidelines=data.brand_guidelines,
|
|
status=data.status,
|
|
)
|
|
if not campaign:
|
|
raise HTTPException(status_code=404, detail="Campaign not found")
|
|
|
|
return CampaignResponse(
|
|
id=campaign.id,
|
|
name=campaign.name,
|
|
workfront_id=campaign.workfront_id,
|
|
client_lead=campaign.client_lead,
|
|
agency_lead=campaign.agency_lead,
|
|
brand_guidelines=campaign.brand_guidelines,
|
|
status=campaign.status,
|
|
agency=campaign.agency.name if campaign.agency else None,
|
|
created_at=campaign.created_at,
|
|
updated_at=campaign.updated_at,
|
|
proofs=len(campaign.proofs) if campaign.proofs else 0,
|
|
)
|
|
|
|
|
|
@router.delete("/campaigns/{campaign_id}", status_code=204)
|
|
async def delete_campaign(
|
|
campaign_id: uuid.UUID,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Delete a campaign and all associated files."""
|
|
repo = CampaignRepository(db)
|
|
|
|
# Get campaign with proofs and versions to extract file keys
|
|
campaign = await repo.get_by_id(campaign_id)
|
|
if not campaign:
|
|
raise HTTPException(status_code=404, detail="Campaign not found")
|
|
|
|
# Delete files from storage for all proofs
|
|
for proof in campaign.proofs:
|
|
for version in proof.versions:
|
|
if version.file_storage_key:
|
|
await storage_service.delete_file(version.file_storage_key)
|
|
|
|
# Delete database records (cascades to proofs and versions)
|
|
await repo.delete(campaign_id)
|
|
|
|
|
|
# Proof endpoints
|
|
@router.get("/campaigns/{campaign_id}/proofs", response_model=list[ProofResponse])
|
|
async def list_proofs(
|
|
campaign_id: uuid.UUID,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""List all proofs for a campaign."""
|
|
repo = ProofRepository(db)
|
|
proofs = await repo.list_by_campaign(campaign_id)
|
|
|
|
return [
|
|
ProofResponse(
|
|
id=proof.id,
|
|
proof_name=proof.proof_name,
|
|
channel=proof.channel,
|
|
sub_channel=proof.sub_channel,
|
|
proof_type=proof.proof_type,
|
|
workfront_id=proof.workfront_id,
|
|
created_at=proof.created_at,
|
|
versions=[
|
|
ProofVersionResponse(
|
|
id=v.id,
|
|
version=v.version,
|
|
file_storage_key=v.file_storage_key,
|
|
thumbnail_url=v.thumbnail_url,
|
|
agent_review=v.agent_review,
|
|
overall_status=v.overall_status,
|
|
workfront_id=v.workfront_id,
|
|
created_at=v.created_at,
|
|
)
|
|
for v in proof.versions
|
|
],
|
|
)
|
|
for proof in proofs
|
|
]
|
|
|
|
|
|
@router.get("/proofs/{proof_id}", response_model=ProofResponse)
|
|
async def get_proof(
|
|
proof_id: uuid.UUID,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get a proof by ID."""
|
|
repo = ProofRepository(db)
|
|
proof = await repo.get_by_id(proof_id)
|
|
if not proof:
|
|
raise HTTPException(status_code=404, detail="Proof not found")
|
|
|
|
return ProofResponse(
|
|
id=proof.id,
|
|
proof_name=proof.proof_name,
|
|
channel=proof.channel,
|
|
sub_channel=proof.sub_channel,
|
|
proof_type=proof.proof_type,
|
|
workfront_id=proof.workfront_id,
|
|
created_at=proof.created_at,
|
|
versions=[
|
|
ProofVersionResponse(
|
|
id=v.id,
|
|
version=v.version,
|
|
file_storage_key=v.file_storage_key,
|
|
thumbnail_url=v.thumbnail_url,
|
|
agent_review=v.agent_review,
|
|
overall_status=v.overall_status,
|
|
workfront_id=v.workfront_id,
|
|
created_at=v.created_at,
|
|
)
|
|
for v in proof.versions
|
|
],
|
|
)
|
|
|
|
|
|
@router.delete("/proofs/{proof_id}", status_code=204)
|
|
async def delete_proof(
|
|
proof_id: uuid.UUID,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Delete a proof and its associated files."""
|
|
repo = ProofRepository(db)
|
|
|
|
# Get proof with versions to extract file keys
|
|
proof = await repo.get_by_id(proof_id)
|
|
if not proof:
|
|
raise HTTPException(status_code=404, detail="Proof not found")
|
|
|
|
# Delete files from storage
|
|
for version in proof.versions:
|
|
if version.file_storage_key:
|
|
await storage_service.delete_file(version.file_storage_key)
|
|
|
|
# Delete database records
|
|
await repo.delete(proof_id)
|
|
|
|
|
|
# Audit endpoints
|
|
@router.post("/proofs/{proof_id}/versions/{version}/flag", response_model=FlaggedItemResponse, status_code=201)
|
|
async def flag_proof_version(
|
|
proof_id: uuid.UUID,
|
|
version: int,
|
|
data: FlaggedItemCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Flag an issue on a proof version."""
|
|
user_id = await get_db_user(db, user)
|
|
proof_repo = ProofRepository(db)
|
|
audit_repo = AuditRepository(db)
|
|
|
|
# Get the proof version
|
|
proof_version = await proof_repo.get_version(proof_id, version)
|
|
if not proof_version:
|
|
raise HTTPException(status_code=404, detail="Proof version not found")
|
|
|
|
flagged = await audit_repo.create_flagged_item(
|
|
proof_version_id=proof_version.id,
|
|
agent_flagged=data.agent_flagged,
|
|
comments=data.comments,
|
|
submitter_id=user_id,
|
|
)
|
|
|
|
# Get related data for response
|
|
proof = await proof_repo.get_by_id(proof_id)
|
|
|
|
return FlaggedItemResponse(
|
|
id=flagged.id,
|
|
proof_version_id=flagged.proof_version_id,
|
|
agent_flagged=flagged.agent_flagged,
|
|
comments=flagged.comments,
|
|
submitter_name=user.get("name"),
|
|
submitter_agency=None,
|
|
campaign_name=proof.campaign.name if proof and proof.campaign else None,
|
|
proof_name=proof.proof_name if proof else None,
|
|
version=version,
|
|
created_at=flagged.created_at,
|
|
)
|
|
|
|
|
|
@router.post("/proofs/{proof_id}/versions/{version}/resolve", response_model=ResolvedItemResponse, status_code=201)
|
|
async def resolve_proof_version(
|
|
proof_id: uuid.UUID,
|
|
version: int,
|
|
data: ResolvedItemCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Resolve an issue on a proof version."""
|
|
user_id = await get_db_user(db, user)
|
|
proof_repo = ProofRepository(db)
|
|
audit_repo = AuditRepository(db)
|
|
|
|
# Get the proof version
|
|
proof_version = await proof_repo.get_version(proof_id, version)
|
|
if not proof_version:
|
|
raise HTTPException(status_code=404, detail="Proof version not found")
|
|
|
|
resolved = await audit_repo.create_resolved_item(
|
|
proof_version_id=proof_version.id,
|
|
agent=data.agent,
|
|
issue=data.issue,
|
|
resolution=data.resolution,
|
|
submitter_id=user_id,
|
|
)
|
|
|
|
# Get related data for response
|
|
proof = await proof_repo.get_by_id(proof_id)
|
|
|
|
return ResolvedItemResponse(
|
|
id=resolved.id,
|
|
proof_version_id=resolved.proof_version_id,
|
|
agent=resolved.agent,
|
|
issue=resolved.issue,
|
|
resolution=resolved.resolution,
|
|
submitter_name=user.get("name"),
|
|
submitter_agency=None,
|
|
campaign_name=proof.campaign.name if proof and proof.campaign else None,
|
|
proof_name=proof.proof_name if proof else None,
|
|
version=version,
|
|
created_at=resolved.created_at,
|
|
)
|
|
|
|
|
|
@router.get("/audit/flagged", response_model=list[FlaggedItemResponse])
|
|
async def list_flagged_items(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
limit: int = Query(100, ge=1, le=500),
|
|
offset: int = Query(0, ge=0),
|
|
):
|
|
"""List all flagged items."""
|
|
audit_repo = AuditRepository(db)
|
|
flagged_items = await audit_repo.get_flagged_items(limit=limit, offset=offset)
|
|
|
|
return [
|
|
FlaggedItemResponse(
|
|
id=item.id,
|
|
proof_version_id=item.proof_version_id,
|
|
agent_flagged=item.agent_flagged,
|
|
comments=item.comments,
|
|
submitter_name=item.submitter.name if item.submitter else None,
|
|
submitter_agency=item.submitter.agency.name if item.submitter and item.submitter.agency else None,
|
|
campaign_name=item.proof_version.proof.campaign.name if item.proof_version and item.proof_version.proof and item.proof_version.proof.campaign else None,
|
|
proof_name=item.proof_version.proof.proof_name if item.proof_version and item.proof_version.proof else None,
|
|
version=item.proof_version.version if item.proof_version else None,
|
|
created_at=item.created_at,
|
|
)
|
|
for item in flagged_items
|
|
]
|
|
|
|
|
|
@router.get("/audit/resolved", response_model=list[ResolvedItemResponse])
|
|
async def list_resolved_items(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
limit: int = Query(100, ge=1, le=500),
|
|
offset: int = Query(0, ge=0),
|
|
):
|
|
"""List all resolved items."""
|
|
audit_repo = AuditRepository(db)
|
|
resolved_items = await audit_repo.get_resolved_items(limit=limit, offset=offset)
|
|
|
|
return [
|
|
ResolvedItemResponse(
|
|
id=item.id,
|
|
proof_version_id=item.proof_version_id,
|
|
agent=item.agent,
|
|
issue=item.issue,
|
|
resolution=item.resolution,
|
|
submitter_name=item.submitter.name if item.submitter else None,
|
|
submitter_agency=item.submitter.agency.name if item.submitter and item.submitter.agency else None,
|
|
campaign_name=item.proof_version.proof.campaign.name if item.proof_version and item.proof_version.proof and item.proof_version.proof.campaign else None,
|
|
proof_name=item.proof_version.proof.proof_name if item.proof_version and item.proof_version.proof else None,
|
|
version=item.proof_version.version if item.proof_version else None,
|
|
created_at=item.created_at,
|
|
)
|
|
for item in resolved_items
|
|
]
|
|
|
|
|
|
@router.get("/audit/errors", response_model=list[ErrorItemResponse])
|
|
async def list_error_items(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
limit: int = Query(100, ge=1, le=500),
|
|
offset: int = Query(0, ge=0),
|
|
):
|
|
"""List all error items."""
|
|
audit_repo = AuditRepository(db)
|
|
error_items = await audit_repo.get_error_items(limit=limit, offset=offset)
|
|
|
|
return [
|
|
ErrorItemResponse(
|
|
id=item.id,
|
|
proof_version_id=item.proof_version_id,
|
|
error_summary=item.error_summary,
|
|
campaign_name=item.proof_version.proof.campaign.name if item.proof_version and item.proof_version.proof and item.proof_version.proof.campaign else None,
|
|
proof_name=item.proof_version.proof.proof_name if item.proof_version and item.proof_version.proof else None,
|
|
version=item.proof_version.version if item.proof_version else None,
|
|
created_at=item.created_at,
|
|
)
|
|
for item in error_items
|
|
]
|
|
|
|
|
|
# Analytics endpoint
|
|
@router.get("/analytics", response_model=AnalyticsResponse)
|
|
async def get_analytics(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get analytics data."""
|
|
repo = CampaignRepository(db)
|
|
analytics = await repo.get_analytics()
|
|
return AnalyticsResponse(**analytics)
|
|
|
|
|
|
# Users endpoint (admin only)
|
|
@router.get("/users", response_model=list[UserResponse])
|
|
async def list_users(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""List all users (admin only)."""
|
|
user_repo = UserRepository(db)
|
|
users = await user_repo.list_all()
|
|
|
|
return [
|
|
UserResponse(
|
|
id=u.id,
|
|
email=u.email,
|
|
name=u.name,
|
|
role=u.role,
|
|
agency=u.agency.name if u.agency else None,
|
|
created_at=u.created_at,
|
|
)
|
|
for u in users
|
|
]
|
|
|
|
|
|
# Dropdown options endpoints
|
|
@router.get("/dropdown-options", response_model=DropdownOptionsResponse)
|
|
async def get_dropdown_options(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get all dropdown options as hierarchical structure."""
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
repo = DropdownRepository(db)
|
|
options = await repo.get_all_hierarchical()
|
|
|
|
# Debug logging
|
|
channels = options.get("channels", {})
|
|
social = channels.get("Social", {})
|
|
meta_proof_types = social.get("Meta", [])
|
|
logger.info(f"[DEBUG API] Returning dropdown options - Social.Meta has {len(meta_proof_types)} proof types: {meta_proof_types}")
|
|
|
|
return DropdownOptionsResponse(**options)
|
|
|
|
|
|
@router.post("/dropdown-options/channels", status_code=201)
|
|
async def add_channel(
|
|
name: str = Query(..., description="Channel name"),
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Add a new channel."""
|
|
repo = DropdownRepository(db)
|
|
await repo.add_channel(name)
|
|
await db.commit()
|
|
return {"message": f"Channel '{name}' added successfully"}
|
|
|
|
|
|
@router.post("/dropdown-options/channels/{channel}/sub-channels", status_code=201)
|
|
async def add_sub_channel(
|
|
channel: str,
|
|
name: str = Query(..., description="Sub-channel name"),
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Add a sub-channel under a channel."""
|
|
repo = DropdownRepository(db)
|
|
result = await repo.add_sub_channel(channel, name)
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail=f"Channel '{channel}' not found")
|
|
await db.commit()
|
|
return {"message": f"Sub-channel '{name}' added to '{channel}'"}
|
|
|
|
|
|
@router.post("/dropdown-options/channels/{channel}/sub-channels/{sub_channel}/proof-types", status_code=201)
|
|
async def add_proof_type(
|
|
channel: str,
|
|
sub_channel: str,
|
|
name: str = Query(..., description="Proof type name"),
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Add a proof type under a sub-channel."""
|
|
repo = DropdownRepository(db)
|
|
result = await repo.add_proof_type(channel, sub_channel, name)
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail=f"Channel '{channel}' or sub-channel '{sub_channel}' not found")
|
|
await db.commit()
|
|
return {"message": f"Proof type '{name}' added to '{channel}/{sub_channel}'"}
|
|
|
|
|
|
@router.delete("/dropdown-options/channels/{channel}", status_code=204)
|
|
async def delete_channel(
|
|
channel: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Delete a channel and all its sub-channels and proof types."""
|
|
repo = DropdownRepository(db)
|
|
success = await repo.remove_channel(channel)
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail=f"Channel '{channel}' not found")
|
|
await db.commit()
|
|
|
|
|
|
@router.delete("/dropdown-options/channels/{channel}/sub-channels/{sub_channel}", status_code=204)
|
|
async def delete_sub_channel(
|
|
channel: str,
|
|
sub_channel: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Delete a sub-channel and all its proof types."""
|
|
repo = DropdownRepository(db)
|
|
success = await repo.remove_sub_channel(channel, sub_channel)
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail=f"Sub-channel '{sub_channel}' not found in channel '{channel}'")
|
|
await db.commit()
|
|
|
|
|
|
@router.delete("/dropdown-options/channels/{channel}/sub-channels/{sub_channel}/proof-types/{proof_type}", status_code=204)
|
|
async def delete_proof_type(
|
|
channel: str,
|
|
sub_channel: str,
|
|
proof_type: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Delete a proof type."""
|
|
repo = DropdownRepository(db)
|
|
success = await repo.remove_proof_type(channel, sub_channel, proof_type)
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail=f"Proof type '{proof_type}' not found")
|
|
await db.commit()
|
|
|
|
|
|
# Agency endpoints
|
|
@router.get("/agencies", response_model=list[AgencyResponse])
|
|
async def list_agencies(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""List all agencies."""
|
|
from sqlalchemy import select
|
|
from app.models.models import Agency
|
|
|
|
stmt = select(Agency).order_by(Agency.name)
|
|
result = await db.execute(stmt)
|
|
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"}
|