fix(pdf): let PdfService reach the PO export route past auth middleware
All checks were successful
PR checks / checks (pull_request) Successful in 49s
PR checks / integration (pull_request) Successful in 31s

"Email PO to vendor" (issue #14) relies on PdfService fetching
/api/po/<id>/export?...&svc=<token> WITHOUT a user session, authenticating
with a `svc` token that matches PDF_SERVICE_TOKEN. The route handler validates
that token, but the auth middleware runs first and its matcher doesn't exempt
the export route — so every unauthenticated fetch was redirected to /login
(307) and the svc bypass never executed. Net effect: the feature could never
render a real PDF on any deployed env, even with the service configured.

Fix: middleware now lets exactly `/api/po/<id>/export` through when its `svc`
query param matches `process.env.PDF_SERVICE_TOKEN` (the route handler still
re-validates it — defense in depth). Everything else stays auth-gated. The
match lives in a dependency-free, edge-safe, unit-tested helper
(lib/pdf-export-auth.ts); middleware already reads server env at runtime via
auth()/NEXTAUTH_SECRET, so reading PDF_SERVICE_TOKEN there is consistent.

Verified on a running build: correct svc + real PO -> 200, correct svc + bogus
PO -> 404 (handler ran), wrong/no svc -> 307 (still gated). 324 unit tests
green; tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hardik 2026-06-24 14:55:40 +05:30
parent 2fcb207add
commit d1af1e6b12
3 changed files with 59 additions and 0 deletions

View file

@ -0,0 +1,24 @@
// Service-token auth for the PO export route, shared by the auth middleware and
// (conceptually) the export route handler.
//
// PdfService ("Email PO to vendor", issue #14) fetches `/api/po/<id>/export`
// WITHOUT a user session, authenticating with a `svc` query param that must equal
// PDF_SERVICE_TOKEN. The route handler validates that token, but the auth
// middleware runs first and would otherwise redirect the unauthenticated request
// to /login — so the middleware uses this to let exactly that one route through
// when the token matches.
//
// Kept dependency-free so it's safe to import into the Edge middleware and easy to
// unit-test. `token` is `process.env.PDF_SERVICE_TOKEN` (undefined when the PDF
// service isn't configured → always denied).
const EXPORT_PATH = /^\/api\/po\/[^/]+\/export\/?$/;
export function isPdfExportServiceRequest(
pathname: string,
svc: string | null | undefined,
token: string | undefined
): boolean {
if (!token || !svc) return false;
if (svc !== token) return false;
return EXPORT_PATH.test(pathname);
}

View file

@ -1,11 +1,20 @@
import { auth } from "@/auth";
import { NextResponse } from "next/server";
import { isPdfExportServiceRequest } from "@/lib/pdf-export-auth";
export default auth((req) => {
const isAuthenticated = !!req.auth;
const pathname = req.nextUrl.pathname;
const isLoginPage = pathname === "/login";
// PdfService fetches the PO export page unauthenticated, using a `svc` token
// that matches PDF_SERVICE_TOKEN (the route handler re-validates it). Let that
// one route through so the service token isn't bounced to /login by the gate
// below. Everything else stays auth-protected.
if (isPdfExportServiceRequest(pathname, req.nextUrl.searchParams.get("svc"), process.env.PDF_SERVICE_TOKEN)) {
return NextResponse.next();
}
if (!isAuthenticated && !isLoginPage) {
const loginUrl = new URL("/login", req.url);
loginUrl.searchParams.set("callbackUrl", pathname);

View file

@ -0,0 +1,26 @@
import { describe, it, expect } from "vitest";
import { isPdfExportServiceRequest } from "@/lib/pdf-export-auth";
const TOKEN = "a".repeat(64);
describe("isPdfExportServiceRequest", () => {
it("allows the export route when the svc token matches", () => {
expect(isPdfExportServiceRequest("/api/po/cmqrug123/export", TOKEN, TOKEN)).toBe(true);
expect(isPdfExportServiceRequest("/api/po/cmqrug123/export/", TOKEN, TOKEN)).toBe(true); // trailing slash
});
it("denies when the token is missing, empty, or wrong", () => {
expect(isPdfExportServiceRequest("/api/po/x/export", TOKEN, undefined)).toBe(false); // service not configured
expect(isPdfExportServiceRequest("/api/po/x/export", null, TOKEN)).toBe(false); // no svc on request
expect(isPdfExportServiceRequest("/api/po/x/export", "", TOKEN)).toBe(false);
expect(isPdfExportServiceRequest("/api/po/x/export", "wrong", TOKEN)).toBe(false);
});
it("only matches the PO export route, not other paths", () => {
expect(isPdfExportServiceRequest("/api/po/x/export/extra", TOKEN, TOKEN)).toBe(false);
expect(isPdfExportServiceRequest("/api/po/x", TOKEN, TOKEN)).toBe(false);
expect(isPdfExportServiceRequest("/dashboard", TOKEN, TOKEN)).toBe(false);
expect(isPdfExportServiceRequest("/api/reports/spend", TOKEN, TOKEN)).toBe(false);
expect(isPdfExportServiceRequest("/api/po//export", TOKEN, TOKEN)).toBe(false); // empty id
});
});