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>
This commit is contained in:
Vadym Samoilenko 2026-03-20 18:46:45 +00:00
parent 25b70af9fb
commit 864278a0fa
12 changed files with 52 additions and 25 deletions

View file

@ -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())

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -55,6 +55,7 @@ export class DashboardApi {
`/api/v1/ppt/presentation/${id}`,
{
method: "GET",
headers: getHeader(),
}
);

View file

@ -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<string, string>;
};

View file

@ -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<ImageAssetResponse[]> {
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) {

View file

@ -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' }
);

View file

@ -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');

View file

@ -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 });

View file

@ -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();
}

View file

@ -62,7 +62,7 @@ const presentationGenerationSlice = createSlice({
setSelectedTemplateId: (state, action: PayloadAction<string | null>) => {
state.selectedTemplateId = action.payload;
},
// Slides rendereimport { useEffect } from "react"d
// Slides rendered
setSlidesRendered: (state, action: PayloadAction<boolean>) => {
state.isSlidesRendered = action.payload;
},