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}`;
}