ppt-tool/backend/api/v1/ppt/endpoints/outlines.py
Vadym Samoilenko 864278a0fa Comprehensive audit: fix auth, basePath, security, and UI bugs
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>
2026-03-20 18:46:45 +00:00

145 lines
5.5 KiB
Python

import asyncio
import json
import math
import traceback
import uuid
import dirtyjson
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from models.presentation_outline_model import PresentationOutlineModel
from models.sql.presentation import PresentationModel
from models.sql.user import UserModel
from models.sse_response import (
SSECompleteResponse,
SSEErrorResponse,
SSEResponse,
SSEStatusResponse,
)
from services.temp_file_service import TEMP_FILE_SERVICE
from services.database import get_async_session, async_session_maker
from services.documents_loader import DocumentsLoader
from utils.auth_dependencies import get_current_user
from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline
from utils.ppt_utils import get_presentation_title_from_outlines
OUTLINES_ROUTER = APIRouter(prefix="/outlines", tags=["Outlines"])
@OUTLINES_ROUTER.get("/stream/{id}")
async def stream_outlines(
id: uuid.UUID,
sql_session: AsyncSession = Depends(get_async_session),
_current_user: UserModel = Depends(get_current_user),
):
presentation = await sql_session.get(PresentationModel, id)
if not presentation:
raise HTTPException(status_code=404, detail="Presentation not found")
# Capture all data from the presentation before the endpoint returns
# (Depends session closes when endpoint function returns, before StreamingResponse runs)
presentation_id = presentation.id
presentation_content = presentation.content
presentation_file_paths = presentation.file_paths
presentation_n_slides = presentation.n_slides
presentation_language = presentation.language
presentation_tone = presentation.tone
presentation_verbosity = presentation.verbosity
presentation_instructions = presentation.instructions
presentation_include_title_slide = presentation.include_title_slide
presentation_include_table_of_contents = presentation.include_table_of_contents
presentation_web_search = presentation.web_search
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
async def inner():
yield SSEStatusResponse(
status="Generating presentation outlines..."
).to_string()
additional_context = ""
if presentation_file_paths:
documents_loader = DocumentsLoader(file_paths=presentation_file_paths)
await documents_loader.load_documents(temp_dir)
documents = documents_loader.documents
if documents:
additional_context = "\n\n".join(documents)
presentation_outlines_text = ""
n_slides_to_generate = presentation_n_slides
if presentation_include_table_of_contents:
needed_toc_count = math.ceil((presentation_n_slides - 1) / 10)
n_slides_to_generate -= math.ceil(
(presentation_n_slides - needed_toc_count) / 10
)
async for chunk in generate_ppt_outline(
presentation_content,
n_slides_to_generate,
presentation_language,
additional_context,
presentation_tone,
presentation_verbosity,
presentation_instructions,
presentation_include_title_slide,
presentation_web_search,
):
# Give control to the event loop
await asyncio.sleep(0)
if isinstance(chunk, HTTPException):
yield SSEErrorResponse(detail=chunk.detail).to_string()
return
yield SSEResponse(
event="response",
data=json.dumps({"type": "chunk", "chunk": chunk}),
).to_string()
presentation_outlines_text += chunk
try:
presentation_outlines_json = dict(
dirtyjson.loads(presentation_outlines_text)
)
# Fix: LLM sometimes returns slides as JSON string instead of list
if "slides" in presentation_outlines_json and isinstance(presentation_outlines_json["slides"], str):
print("[OUTLINE] Warning: slides field is a string, parsing as JSON...")
presentation_outlines_json["slides"] = dirtyjson.loads(presentation_outlines_json["slides"])
except Exception as e:
traceback.print_exc()
yield SSEErrorResponse(
detail=f"Failed to generate presentation outlines. Please try again. {str(e)}",
).to_string()
return
presentation_outlines = PresentationOutlineModel(**presentation_outlines_json)
presentation_outlines.slides = presentation_outlines.slides[
:n_slides_to_generate
]
outlines_data = presentation_outlines.model_dump()
title = get_presentation_title_from_outlines(presentation_outlines)
# Use a fresh session for the DB write — Depends session is already closed
async with async_session_maker() as db:
pres = await db.get(PresentationModel, presentation_id)
if pres:
pres.outlines = outlines_data
pres.title = title
db.add(pres)
await db.commit()
await db.refresh(pres)
yield SSECompleteResponse(
key="presentation", value=pres.model_dump(mode="json")
).to_string()
else:
yield SSEErrorResponse(detail="Presentation not found after outline generation").to_string()
return StreamingResponse(inner(), media_type="text/event-stream")