From 5766e252aaab8925d404fb76ba9ca8a99334a04a Mon Sep 17 00:00:00 2001 From: sudipnext Date: Mon, 27 Apr 2026 21:13:57 +0545 Subject: [PATCH] feat: Implement PDF export functionality with session handling - Added middleware to handle session cookies for the PDF export route. - Introduced a new API endpoint for exporting presentation data using session cookies. - Updated the PdfMakerPage component to accept and utilize the export cookie. - Enhanced the presentation export logic to include session token extraction from cookies. - Updated routing configuration to include the new PDF maker path. --- .../app/(export)/pdf-maker/PdfMakerPage.tsx | 37 ++++++++++- .../nextjs/app/(export)/pdf-maker/page.tsx | 3 +- .../export-presentation-data/[id]/route.ts | 62 +++++++++++++++++++ .../lib/run-bundled-presentation-export.ts | 22 ++++++- servers/nextjs/middleware.ts | 33 +++++++++- servers/nextjs/utils/api.ts | 8 +-- 6 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 servers/nextjs/app/api/export-presentation-data/[id]/route.ts diff --git a/servers/nextjs/app/(export)/pdf-maker/PdfMakerPage.tsx b/servers/nextjs/app/(export)/pdf-maker/PdfMakerPage.tsx index 88826b6c..736a45f2 100644 --- a/servers/nextjs/app/(export)/pdf-maker/PdfMakerPage.tsx +++ b/servers/nextjs/app/(export)/pdf-maker/PdfMakerPage.tsx @@ -11,6 +11,7 @@ import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; import { AlertCircle } from "lucide-react"; import { setPresentationData } from "@/store/slices/presentationGeneration"; import { DashboardApi } from "@/app/(presentation-generator)/services/api/dashboard"; +import { ApiResponseHandler } from "@/app/(presentation-generator)/services/api/api-error-handler"; import { setupImageUrlConverter } from "@/utils/image-url-converter"; import { V1ContentRender } from "@/app/(presentation-generator)/components/V1ContentRender"; @@ -21,9 +22,21 @@ import { Theme } from "@/app/(presentation-generator)/services/api/types"; -const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { +type PresentationPageProps = { + presentation_id: string; + exportCookie?: string; +}; + +const PresentationPage = ({ presentation_id, exportCookie }: PresentationPageProps) => { const pathname = usePathname(); const [contentLoading, setContentLoading] = useState(true); + const exportCookieFromHash = + typeof window !== "undefined" + ? new URLSearchParams(window.location.hash.replace(/^#/, "")).get( + "exportCookie" + ) ?? undefined + : undefined; + const effectiveExportCookie = exportCookie ?? exportCookieFromHash; const dispatch = useDispatch(); const { presentationData } = useSelector( @@ -55,7 +68,9 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { // Function to fetch the user slides const fetchUserSlides = async () => { try { - const data = await DashboardApi.getPresentation(presentation_id); + const data = effectiveExportCookie + ? await fetchPresentationForExport(presentation_id, effectiveExportCookie) + : await DashboardApi.getPresentation(presentation_id); dispatch(setPresentationData(data)); setContentLoading(false); @@ -78,6 +93,24 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { } }; + const fetchPresentationForExport = async ( + id: string, + cookieHeader: string + ) => { + const response = await fetch(`/api/export-presentation-data/${id}`, { + method: "GET", + headers: { + "x-export-cookie": cookieHeader, + }, + cache: "no-store", + }); + + return ApiResponseHandler.handleResponse( + response, + "Presentation not found" + ); + }; + const applyTheme = async (theme: Theme) => { const element = document.getElementById('presentation-slides-wrapper') if (!element) return; diff --git a/servers/nextjs/app/(export)/pdf-maker/page.tsx b/servers/nextjs/app/(export)/pdf-maker/page.tsx index 8f536171..e7427ada 100644 --- a/servers/nextjs/app/(export)/pdf-maker/page.tsx +++ b/servers/nextjs/app/(export)/pdf-maker/page.tsx @@ -9,6 +9,7 @@ const page = () => { const router = useRouter(); const params = useSearchParams(); const queryId = params.get("id"); + const exportCookie = params.get("exportCookie") ?? undefined; if (!queryId) { return (
@@ -19,7 +20,7 @@ const page = () => { ); } return ( - + ); }; export default page; diff --git a/servers/nextjs/app/api/export-presentation-data/[id]/route.ts b/servers/nextjs/app/api/export-presentation-data/[id]/route.ts new file mode 100644 index 00000000..1c5b6b2e --- /dev/null +++ b/servers/nextjs/app/api/export-presentation-data/[id]/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from "next/server"; + +function getFastApiBaseUrl(): string { + const internal = process.env.FAST_API_INTERNAL_URL?.trim(); + if (internal) { + return internal.replace(/\/+$/, ""); + } + + const configured = process.env.NEXT_PUBLIC_FAST_API?.trim(); + if (configured) { + return configured.replace(/\/+$/, ""); + } + + return "http://127.0.0.1:8000"; +} + +export async function GET( + request: NextRequest, + context: { params: { id: string } } +) { + const { id } = context.params; + if (!id) { + return NextResponse.json( + { detail: "Missing presentation id" }, + { status: 400 } + ); + } + + const exportCookie = request.headers.get("x-export-cookie")?.trim(); + if (!exportCookie) { + return NextResponse.json({ detail: "Unauthorized" }, { status: 401 }); + } + + const presentationUrl = `${getFastApiBaseUrl()}/api/v1/ppt/presentation/${id}`; + + try { + const response = await fetch(presentationUrl, { + method: "GET", + headers: { + Cookie: exportCookie, + }, + cache: "no-store", + }); + + const bodyText = await response.text(); + const contentType = response.headers.get("content-type") ?? "application/json"; + + return new NextResponse(bodyText, { + status: response.status, + headers: { + "Content-Type": contentType, + "Cache-Control": "no-store", + }, + }); + } catch (error) { + console.error("[export-presentation-data] Failed to fetch presentation", error); + return NextResponse.json( + { detail: "Failed to fetch presentation data" }, + { status: 500 } + ); + } +} diff --git a/servers/nextjs/lib/run-bundled-presentation-export.ts b/servers/nextjs/lib/run-bundled-presentation-export.ts index 15b8ec34..01e53b3f 100644 --- a/servers/nextjs/lib/run-bundled-presentation-export.ts +++ b/servers/nextjs/lib/run-bundled-presentation-export.ts @@ -19,6 +19,19 @@ export function getPresentonAppRoot(): string { ); } +function extractSessionTokenFromCookieHeader(cookieHeader?: string): string | undefined { + if (!cookieHeader) { + return undefined; + } + + const match = cookieHeader.match(/(?:^|;\s*)presenton_session=([^;]+)/); + if (!match?.[1]) { + return undefined; + } + + return decodeURIComponent(match[1]); +} + async function resolveExportEntrypoint(exportRoot: string): Promise { const indexCjs = path.join(exportRoot, "index.cjs"); const indexJs = path.join(exportRoot, "index.js"); @@ -129,11 +142,18 @@ export async function runBundledPresentationExport(params: { const nextjsUrl = process.env.NEXT_PUBLIC_URL?.trim() || "http://127.0.0.1"; const q = new URLSearchParams({ id: presentationId }); + const sessionToken = extractSessionTokenFromCookieHeader(cookieHeader); + if (sessionToken) { + q.set("exportSession", sessionToken); + } const fastapiUrl = process.env.NEXT_PUBLIC_FAST_API?.trim(); if (fastapiUrl) { q.set("fastapiUrl", fastapiUrl); } - const pptUrl = `${nextjsUrl}/pdf-maker?${q.toString()}`; + const basePptUrl = `${nextjsUrl}/pdf-maker?${q.toString()}`; + const pptUrl = cookieHeader?.trim() + ? `${basePptUrl}#exportCookie=${encodeURIComponent(cookieHeader)}` + : basePptUrl; const tempBase = process.env.TEMP_DIRECTORY?.trim() || path.join(os.tmpdir(), "presenton"); diff --git a/servers/nextjs/middleware.ts b/servers/nextjs/middleware.ts index 5d68e158..cd79b265 100644 --- a/servers/nextjs/middleware.ts +++ b/servers/nextjs/middleware.ts @@ -24,6 +24,9 @@ type AuthStatus = { authenticated: boolean; }; +const SESSION_COOKIE_NAME = "presenton_session"; +const SESSION_TTL_SECONDS = 60 * 60 * 24 * 30; + async function getAuthStatus(request: NextRequest): Promise { const cookieHeader = request.headers.get("cookie"); const authStatusUrl = `${getFastApiBaseUrl(request)}/api/v1/auth/status`; @@ -48,13 +51,39 @@ async function getAuthStatus(request: NextRequest): Promise { function isApiAuthExempt(pathname: string): boolean { return ( - pathname.startsWith("/api/v1/auth/") || pathname === "/api/telemetry-status" + pathname.startsWith("/api/v1/auth/") || + pathname === "/api/telemetry-status" || + pathname.startsWith("/api/export-presentation-data/") ); } export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; + if (pathname === "/pdf-maker") { + const exportSession = request.nextUrl.searchParams.get("exportSession"); + if (exportSession) { + const redirectUrl = request.nextUrl.clone(); + redirectUrl.searchParams.delete("exportSession"); + + const response = NextResponse.redirect(redirectUrl); + response.cookies.set({ + name: SESSION_COOKIE_NAME, + value: exportSession, + maxAge: SESSION_TTL_SECONDS, + httpOnly: true, + secure: + request.headers.get("x-forwarded-proto")?.toLowerCase() === "https" || + request.nextUrl.protocol === "https:", + sameSite: "lax", + path: "/", + }); + return response; + } + + return NextResponse.next(); + } + if (request.method === "OPTIONS" || isApiAuthExempt(pathname)) { return NextResponse.next(); } @@ -76,5 +105,5 @@ export async function middleware(request: NextRequest) { } export const config = { - matcher: ["/api/:path*"], + matcher: ["/api/:path*", "/pdf-maker"], }; diff --git a/servers/nextjs/utils/api.ts b/servers/nextjs/utils/api.ts index 4a2f3376..2dd85138 100644 --- a/servers/nextjs/utils/api.ts +++ b/servers/nextjs/utils/api.ts @@ -51,12 +51,12 @@ export function getApiUrl(path: string): string { const normalizedPath = withLeadingSlash(path); const isFastApiEndpoint = normalizedPath.startsWith("/api/v1/"); - const hasConfiguredFastApi = - !!process.env.NEXT_PUBLIC_FAST_API || !!getFastApiUrlFromQuery(); + const hasConfiguredFastApi = !!process.env.NEXT_PUBLIC_FAST_API; // In web/docker, /api/v1 is typically reverse-proxied by the web server. - // If a FastAPI origin is explicitly configured, use it instead of same-origin proxy. - if (isFastApiEndpoint && hasConfiguredFastApi) { + // Keep browser requests same-origin so session cookies stay attached. + // Server-side callers can still use configured FastAPI base URLs directly. + if (isFastApiEndpoint && typeof window === "undefined" && hasConfiguredFastApi) { return `${getFastAPIUrl()}${normalizedPath}`; }