import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; // Next.js 16 renamed `middleware.ts` to `proxy.ts` — this file must live // at the project root (same level as `app/`), not inside `app/`. See // node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/proxy.md. // // Sliding-session refresh: Payload's customers auth token is valid for // 7200s (2h, Payload default — see Customers.ts in the Payload repo, // unchanged). Rather than issuing a long-lived token (harder to reason // about if one ever leaks) or building a separate refresh-token cookie, // this silently extends the *existing* token via Payload's own built-in // refresh-token endpoint whenever it's getting close to expiry and the // customer is actually still browsing — so an active shopper never gets // logged out mid-session, but someone who walks away is logged out within // 2h of their last request, same as before. const SESSION_COOKIE = "ep_customer_token"; const REFRESH_THRESHOLD_MS = 15 * 60 * 1000; // refresh once < 15min remain const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://payload.mk360.de"; function getTokenExpiry(token: string): number | null { try { const payloadSegment = token.split(".")[1]; if (!payloadSegment) return null; // Not verified here — this is only a "should I bother refreshing?" // heuristic. Payload verifies the token for real on every actual API // call regardless of what this function decides. const json = JSON.parse(Buffer.from(payloadSegment, "base64url").toString("utf8")); return typeof json.exp === "number" ? json.exp * 1000 : null; } catch { return null; } } export default async function proxy(request: NextRequest) { const token = request.cookies.get(SESSION_COOKIE)?.value; if (!token) return NextResponse.next(); const expiresAt = getTokenExpiry(token); if (!expiresAt || expiresAt - Date.now() > REFRESH_THRESHOLD_MS) return NextResponse.next(); try { const res = await fetch(`${PAYLOAD_URL}/api/customers/refresh-token`, { method: "POST", headers: { Authorization: `JWT ${token}` }, }); if (!res.ok) return NextResponse.next(); const data: { refreshedToken?: string } = await res.json(); if (!data.refreshedToken) return NextResponse.next(); const response = NextResponse.next(); response.cookies.set(SESSION_COOKIE, data.refreshedToken, { httpOnly: true, secure: true, sameSite: "lax", path: "/", maxAge: 60 * 60 * 2, }); return response; } catch { // Refresh failing (Payload briefly unreachable etc.) shouldn't block // the actual page request — worst case the session just expires // normally and the customer logs in again. return NextResponse.next(); } } export const config = { matcher: ["/checkout", "/konto/:path*", "/api/account/:path*"], };