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.
This commit is contained in:
sudipnext 2026-04-27 21:13:57 +05:45
parent 6ccd223576
commit 5766e252aa
6 changed files with 155 additions and 10 deletions

View file

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

View file

@ -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 (
<div className="flex flex-col items-center justify-center h-screen">
@ -19,7 +20,7 @@ const page = () => {
);
}
return (
<PdfMakerPage presentation_id={queryId} />
<PdfMakerPage presentation_id={queryId} exportCookie={exportCookie} />
);
};
export default page;

View file

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

View file

@ -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<string> {
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");

View file

@ -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<AuthStatus> {
const cookieHeader = request.headers.get("cookie");
const authStatusUrl = `${getFastApiBaseUrl(request)}/api/v1/auth/status`;
@ -48,13 +51,39 @@ async function getAuthStatus(request: NextRequest): Promise<AuthStatus> {
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"],
};

View file

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