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:
parent
6ccd223576
commit
5766e252aa
6 changed files with 155 additions and 10 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue