- Add file_hash and is_identical_file columns to proof_versions table - Compute MD5 hash on file upload and compare with previous version - Display warning banner when uploading identical file as revision - Return is_identical_file in WebSocket response and API endpoints Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
729 lines
24 KiB
Python
Executable file
729 lines
24 KiB
Python
Executable file
"""REST API routes for campaigns, proofs, and audit items."""
|
|
import base64
|
|
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
|
|
from app.services.pdf_service import pdf_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,
|
|
is_identical_file=v.is_identical_file,
|
|
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,
|
|
is_identical_file=v.is_identical_file,
|
|
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]
|
|
|
|
|
|
# PDF pages endpoint (must be defined BEFORE the base file endpoint for correct routing)
|
|
@router.get("/files/{storage_key:path}/pages")
|
|
async def get_pdf_pages(
|
|
storage_key: str,
|
|
max_pages: int = Query(10, ge=1, le=50),
|
|
db: AsyncSession = Depends(get_db),
|
|
user: dict = Depends(get_current_user),
|
|
):
|
|
"""Rasterize a stored PDF and return pages as data URLs."""
|
|
if not storage_key.lower().endswith('.pdf'):
|
|
raise HTTPException(status_code=400, detail="File is not a PDF")
|
|
|
|
file_data = await storage_service.get_file(storage_key)
|
|
if file_data is None:
|
|
raise HTTPException(status_code=404, detail="File not found")
|
|
|
|
try:
|
|
pages = pdf_service.rasterize(file_data, max_pages=max_pages)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
return {
|
|
"pages": [
|
|
{
|
|
"page": i + 1,
|
|
"data_url": f"data:image/png;base64,{base64.b64encode(png_data).decode('utf-8')}",
|
|
"width": width,
|
|
"height": height,
|
|
}
|
|
for i, (png_data, width, height) in enumerate(pages)
|
|
]
|
|
}
|
|
|
|
|
|
# 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"}
|