From 864278a0fa393f590bf8f631bb065e7a2d0868e8 Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Fri, 20 Mar 2026 18:46:45 +0000 Subject: [PATCH] Comprehensive audit: fix auth, basePath, security, and UI bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/api/v1/ppt/endpoints/files.py | 16 ++++++++++--- backend/api/v1/ppt/endpoints/images.py | 24 +++++++++++++++---- backend/api/v1/ppt/endpoints/outlines.py | 6 ++++- backend/api/v1/ppt/endpoints/slide.py | 4 ++++ .../services/api/dashboard.ts | 1 + .../services/api/header.ts | 11 ++------- .../services/api/images.ts | 7 +++--- frontend/app/admin/templates/[id]/page.tsx | 2 +- frontend/app/admin/users/page.tsx | 1 + frontend/app/api/has-required-key/route.ts | 1 - frontend/store/slices/adminSlice.ts | 2 +- .../store/slices/presentationGeneration.ts | 2 +- 12 files changed, 52 insertions(+), 25 deletions(-) diff --git a/backend/api/v1/ppt/endpoints/files.py b/backend/api/v1/ppt/endpoints/files.py index bbe0cde..41cea66 100644 --- a/backend/api/v1/ppt/endpoints/files.py +++ b/backend/api/v1/ppt/endpoints/files.py @@ -46,7 +46,10 @@ def _is_image(file_path: str) -> bool: @FILES_ROUTER.post("/upload", response_model=List[str]) -async def upload_files(files: Optional[List[UploadFile]]): +async def upload_files( + files: Optional[List[UploadFile]], + _current_user: UserModel = Depends(get_current_user), +): if not files: raise HTTPException(status_code=400, detail="Documents are required") @@ -70,7 +73,10 @@ async def upload_files(files: Optional[List[UploadFile]]): @FILES_ROUTER.post("/decompose", response_model=List[DecomposedFileInfo]) -async def decompose_files(file_paths: Annotated[List[str], Body(embed=True)]): +async def decompose_files( + file_paths: Annotated[List[str], Body(embed=True)], + _current_user: UserModel = Depends(get_current_user), +): temp_dir = TEMP_FILE_SERVICE.create_temp_dir(str(uuid.uuid4())) txt_files = [] @@ -200,7 +206,10 @@ class UrlParseResponse(BaseModel): @FILES_ROUTER.post("/url", response_model=UrlParseResponse) -async def parse_url_endpoint(body: UrlParseRequest): +async def parse_url_endpoint( + body: UrlParseRequest, + _current_user: UserModel = Depends(get_current_user), +): """Fetch a URL and extract its article content as text.""" if not body.url or not body.url.strip(): raise HTTPException(status_code=400, detail="URL is required") @@ -236,6 +245,7 @@ async def fetch_url_content( async def update_files( file_path: Annotated[str, Body()], file: Annotated[UploadFile, File()], + _current_user: UserModel = Depends(get_current_user), ): with open(file_path, "wb") as f: f.write(await file.read()) diff --git a/backend/api/v1/ppt/endpoints/images.py b/backend/api/v1/ppt/endpoints/images.py index 62731f5..ca43d6d 100644 --- a/backend/api/v1/ppt/endpoints/images.py +++ b/backend/api/v1/ppt/endpoints/images.py @@ -5,9 +5,11 @@ from sqlmodel import select from models.image_prompt import ImagePrompt from models.sql.image_asset import ImageAsset +from models.sql.user import UserModel from services.database import get_async_session from services.image_generation_service import ImageGenerationService from utils.asset_directory_utils import get_images_directory +from utils.auth_dependencies import get_current_user import os import uuid from utils.file_utils import get_file_name_with_random_uuid @@ -17,7 +19,9 @@ IMAGES_ROUTER = APIRouter(prefix="/images", tags=["Images"]) @IMAGES_ROUTER.get("/generate") async def generate_image( - prompt: str, sql_session: AsyncSession = Depends(get_async_session) + prompt: str, + sql_session: AsyncSession = Depends(get_async_session), + _current_user: UserModel = Depends(get_current_user), ): images_directory = get_images_directory() image_prompt = ImagePrompt(prompt=prompt) @@ -34,7 +38,10 @@ async def generate_image( @IMAGES_ROUTER.get("/generated", response_model=List[ImageAsset]) -async def get_generated_images(sql_session: AsyncSession = Depends(get_async_session)): +async def get_generated_images( + sql_session: AsyncSession = Depends(get_async_session), + _current_user: UserModel = Depends(get_current_user), +): try: images = await sql_session.scalars( select(ImageAsset) @@ -50,7 +57,9 @@ async def get_generated_images(sql_session: AsyncSession = Depends(get_async_ses @IMAGES_ROUTER.post("/upload") async def upload_image( - file: UploadFile = File(...), sql_session: AsyncSession = Depends(get_async_session) + file: UploadFile = File(...), + sql_session: AsyncSession = Depends(get_async_session), + _current_user: UserModel = Depends(get_current_user), ): try: new_filename = get_file_name_with_random_uuid(file) @@ -72,7 +81,10 @@ async def upload_image( @IMAGES_ROUTER.get("/uploaded", response_model=List[ImageAsset]) -async def get_uploaded_images(sql_session: AsyncSession = Depends(get_async_session)): +async def get_uploaded_images( + sql_session: AsyncSession = Depends(get_async_session), + _current_user: UserModel = Depends(get_current_user), +): try: images = await sql_session.scalars( select(ImageAsset) @@ -88,7 +100,9 @@ async def get_uploaded_images(sql_session: AsyncSession = Depends(get_async_sess @IMAGES_ROUTER.delete("/{id}", status_code=204) async def delete_uploaded_image_by_id( - id: uuid.UUID, sql_session: AsyncSession = Depends(get_async_session) + id: uuid.UUID, + sql_session: AsyncSession = Depends(get_async_session), + _current_user: UserModel = Depends(get_current_user), ): try: # Fetch the asset to get its actual file path diff --git a/backend/api/v1/ppt/endpoints/outlines.py b/backend/api/v1/ppt/endpoints/outlines.py index be028cb..ee040fd 100644 --- a/backend/api/v1/ppt/endpoints/outlines.py +++ b/backend/api/v1/ppt/endpoints/outlines.py @@ -10,6 +10,7 @@ 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, @@ -19,6 +20,7 @@ from models.sse_response import ( 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 @@ -27,7 +29,9 @@ 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) + id: uuid.UUID, + sql_session: AsyncSession = Depends(get_async_session), + _current_user: UserModel = Depends(get_current_user), ): presentation = await sql_session.get(PresentationModel, id) diff --git a/backend/api/v1/ppt/endpoints/slide.py b/backend/api/v1/ppt/endpoints/slide.py index 529edf8..5bdd290 100644 --- a/backend/api/v1/ppt/endpoints/slide.py +++ b/backend/api/v1/ppt/endpoints/slide.py @@ -8,8 +8,10 @@ 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 @@ -26,6 +28,7 @@ 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: @@ -92,6 +95,7 @@ async def edit_slide_html( 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: diff --git a/frontend/app/(presentation-generator)/services/api/dashboard.ts b/frontend/app/(presentation-generator)/services/api/dashboard.ts index 8627749..f08fe52 100644 --- a/frontend/app/(presentation-generator)/services/api/dashboard.ts +++ b/frontend/app/(presentation-generator)/services/api/dashboard.ts @@ -55,6 +55,7 @@ export class DashboardApi { `/api/v1/ppt/presentation/${id}`, { method: "GET", + headers: getHeader(), } ); diff --git a/frontend/app/(presentation-generator)/services/api/header.ts b/frontend/app/(presentation-generator)/services/api/header.ts index 41ce691..ea8ef8f 100644 --- a/frontend/app/(presentation-generator)/services/api/header.ts +++ b/frontend/app/(presentation-generator)/services/api/header.ts @@ -1,17 +1,10 @@ export const getHeader = () => { return { "Content-Type": "application/json", - Accept: "application/json", - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization", + Accept: "application/json", }; }; export const getHeaderForFormData = () => { - return { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization", - }; + return {} as Record; }; diff --git a/frontend/app/(presentation-generator)/services/api/images.ts b/frontend/app/(presentation-generator)/services/api/images.ts index fe67307..24b031e 100644 --- a/frontend/app/(presentation-generator)/services/api/images.ts +++ b/frontend/app/(presentation-generator)/services/api/images.ts @@ -1,4 +1,4 @@ -import { getHeaderForFormData } from "./header"; +import { getHeaderForFormData, getHeader } from "./header"; import { apiFetch } from '../../../../lib/apiFetch'; import { ApiResponseHandler } from "./api-error-handler"; import { ImageAssetResponse } from "./types"; @@ -24,7 +24,7 @@ export class ImagesApi { static async getUploadedImages(): Promise { try { - const response = await apiFetch(`/api/v1/ppt/images/uploaded`); + const response = await apiFetch(`/api/v1/ppt/images/uploaded`, { headers: getHeader() }); return await ApiResponseHandler.handleResponse(response, "Failed to get uploaded images") as ImageAssetResponse[]; } catch (error:any) { console.log("Get uploaded images error:", error); @@ -35,7 +35,8 @@ export class ImagesApi { static async deleteImage(image_id: string): Promise<{success: boolean, message?: string}> { try { const response = await apiFetch(`/api/v1/ppt/images/${image_id}`, { - method: "DELETE" + method: "DELETE", + headers: getHeader(), }); return await ApiResponseHandler.handleResponse(response, "Failed to delete image") as {success: boolean, message?: string}; } catch (error:any) { diff --git a/frontend/app/admin/templates/[id]/page.tsx b/frontend/app/admin/templates/[id]/page.tsx index 04baf02..0d7d7c4 100644 --- a/frontend/app/admin/templates/[id]/page.tsx +++ b/frontend/app/admin/templates/[id]/page.tsx @@ -81,7 +81,7 @@ export default function TemplateDetailPage() { } setTogglingId(layout.db_id); try { - const res = await fetch( + const res = await apiFetch( `/api/v1/ppt/template-management/layouts/${layout.db_id}/toggle-enabled`, { method: 'PATCH' } ); diff --git a/frontend/app/admin/users/page.tsx b/frontend/app/admin/users/page.tsx index 689bc63..257360c 100644 --- a/frontend/app/admin/users/page.tsx +++ b/frontend/app/admin/users/page.tsx @@ -47,6 +47,7 @@ export default function UsersPage() { const handleDeactivate = async (userId: string) => { try { await dispatch(deactivateUser(userId)).unwrap(); + dispatch(fetchUsers()); toast.success('User deactivated'); } catch (err) { toast.error(typeof err === 'string' ? err : 'Failed to deactivate user'); diff --git a/frontend/app/api/has-required-key/route.ts b/frontend/app/api/has-required-key/route.ts index 3248e68..fe566e4 100644 --- a/frontend/app/api/has-required-key/route.ts +++ b/frontend/app/api/has-required-key/route.ts @@ -18,7 +18,6 @@ export async function GET() { const keyFromEnv = process.env.OPENAI_API_KEY || ""; - console.log(keyFromEnv); const hasKey = Boolean((keyFromFile || keyFromEnv).trim()); return NextResponse.json({ hasKey }); diff --git a/frontend/store/slices/adminSlice.ts b/frontend/store/slices/adminSlice.ts index 0d3249d..28ba371 100644 --- a/frontend/store/slices/adminSlice.ts +++ b/frontend/store/slices/adminSlice.ts @@ -154,7 +154,7 @@ export const fetchTeams = createAsyncThunk( const url = clientId ? `/api/v1/admin/teams?client_id=${clientId}` : "/api/v1/admin/teams"; - const res = await fetch(url); + const res = await apiFetch(url); if (!res.ok) throw new Error("Failed to fetch teams"); return await res.json(); } diff --git a/frontend/store/slices/presentationGeneration.ts b/frontend/store/slices/presentationGeneration.ts index ee3ac10..cc93dd2 100644 --- a/frontend/store/slices/presentationGeneration.ts +++ b/frontend/store/slices/presentationGeneration.ts @@ -62,7 +62,7 @@ const presentationGenerationSlice = createSlice({ setSelectedTemplateId: (state, action: PayloadAction) => { state.selectedTemplateId = action.payload; }, - // Slides rendereimport { useEffect } from "react"d + // Slides rendered setSlidesRendered: (state, action: PayloadAction) => { state.isSlidesRendered = action.payload; },