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:
parent
25b70af9fb
commit
864278a0fa
12 changed files with 52 additions and 25 deletions
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export class DashboardApi {
|
|||
`/api/v1/ppt/presentation/${id}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: getHeader(),
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue