Backend security (P0): - Add get_current_user auth to all files endpoints (upload, decompose, url, update) - Add get_current_user auth to all images endpoints (generate, upload, uploaded, generated, delete) - Add get_current_user auth to slide edit and edit-html endpoints - Add get_current_user auth to outlines SSE stream endpoint (was fully unauthenticated) Frontend API fixes: - adminSlice fetchTeams: bare fetch() → apiFetch() (was missing basePath prefix) - dashboard getPresentation: add missing getHeader() auth headers - images getUploadedImages/deleteImage: add missing getHeader() auth headers - templates/[id] toggle layout: bare fetch() → apiFetch() (404 in production) - header.ts: remove incorrect client-side CORS headers (Access-Control-Allow-*) UI fixes: - admin/users: add fetchUsers() refetch after deactivate (table wasn't updating) - presentationGeneration.ts: fix corrupt comment with embedded import statement Security: - has-required-key/route.ts: remove console.log() leaking OPENAI_API_KEY to logs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
import asyncio
|
|
import logging
|
|
import os
|
|
from typing import Annotated, Optional
|
|
from fastapi import APIRouter, Body, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
import uuid
|
|
|
|
from models.sql.presentation import PresentationModel
|
|
from models.sql.slide import SlideModel
|
|
from models.sql.user import UserModel
|
|
from services.database import get_async_session
|
|
from services.image_generation_service import ImageGenerationService
|
|
from utils.auth_dependencies import get_current_user
|
|
from utils.asset_directory_utils import get_images_directory
|
|
from utils.llm_calls.edit_slide import get_edited_slide_content
|
|
from utils.llm_calls.edit_slide_html import get_edited_slide_html
|
|
from utils.llm_calls.select_slide_type_on_edit import get_slide_layout_from_prompt
|
|
from utils.process_slides import process_old_and_new_slides_and_fetch_assets
|
|
|
|
|
|
SLIDE_ROUTER = APIRouter(prefix="/slide", tags=["Slide"])
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@SLIDE_ROUTER.post("/edit")
|
|
async def edit_slide(
|
|
id: Annotated[uuid.UUID, Body()],
|
|
prompt: Annotated[str, Body()],
|
|
sql_session: AsyncSession = Depends(get_async_session),
|
|
_current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
slide = await sql_session.get(SlideModel, id)
|
|
if not slide:
|
|
raise HTTPException(status_code=404, detail="Slide not found")
|
|
presentation = await sql_session.get(PresentationModel, slide.presentation)
|
|
if not presentation:
|
|
raise HTTPException(status_code=404, detail="Presentation not found")
|
|
|
|
try:
|
|
presentation_layout = presentation.get_layout()
|
|
slide_layout = await asyncio.wait_for(
|
|
get_slide_layout_from_prompt(prompt, presentation_layout, slide),
|
|
timeout=int(os.getenv("LLM_TIMEOUT_SECONDS", "60")),
|
|
)
|
|
|
|
edited_slide_content = await asyncio.wait_for(
|
|
get_edited_slide_content(
|
|
prompt, slide, presentation.language, slide_layout
|
|
),
|
|
timeout=int(os.getenv("LLM_TIMEOUT_SECONDS", "90")),
|
|
)
|
|
|
|
image_generation_service = ImageGenerationService(get_images_directory())
|
|
|
|
# This will mutate edited_slide_content
|
|
new_assets = await asyncio.wait_for(
|
|
process_old_and_new_slides_and_fetch_assets(
|
|
image_generation_service,
|
|
slide.content,
|
|
edited_slide_content,
|
|
),
|
|
timeout=int(os.getenv("IMAGE_GENERATION_TIMEOUT_SECONDS", "120")),
|
|
)
|
|
except asyncio.TimeoutError:
|
|
raise HTTPException(
|
|
status_code=504,
|
|
detail="Slide editing timed out. The AI model or image generation took too long. Please try again.",
|
|
)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.exception("Failed to edit slide:")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Failed to edit slide: {str(e)}",
|
|
)
|
|
|
|
# Always assign a new unique id to the slide
|
|
slide.id = uuid.uuid4()
|
|
|
|
sql_session.add(slide)
|
|
slide.content = edited_slide_content
|
|
slide.layout = slide_layout.id
|
|
slide.speaker_note = edited_slide_content.get("__speaker_note__", "")
|
|
sql_session.add_all(new_assets)
|
|
await sql_session.commit()
|
|
|
|
return slide
|
|
|
|
|
|
@SLIDE_ROUTER.post("/edit-html", response_model=SlideModel)
|
|
async def edit_slide_html(
|
|
id: Annotated[uuid.UUID, Body()],
|
|
prompt: Annotated[str, Body()],
|
|
html: Annotated[Optional[str], Body()] = None,
|
|
sql_session: AsyncSession = Depends(get_async_session),
|
|
_current_user: UserModel = Depends(get_current_user),
|
|
):
|
|
slide = await sql_session.get(SlideModel, id)
|
|
if not slide:
|
|
raise HTTPException(status_code=404, detail="Slide not found")
|
|
|
|
html_to_edit = html or slide.html_content
|
|
if not html_to_edit:
|
|
raise HTTPException(status_code=400, detail="No HTML to edit")
|
|
|
|
edited_slide_html = await get_edited_slide_html(prompt, html_to_edit)
|
|
|
|
# Always assign a new unique id to the slide
|
|
# This is to ensure that the nextjs can track slide updates
|
|
slide.id = uuid.uuid4()
|
|
|
|
sql_session.add(slide)
|
|
slide.html_content = edited_slide_html
|
|
await sql_session.commit()
|
|
|
|
return slide
|