/** * Portal authentication middleware. * * Customer portal pages (/portal/*) are accessed via signed JWT links that * are emailed to customers. No password is needed — the link IS the credential. * * Token format (JWT, HS256): * payload: { order_id, order_type, email, iat, exp } * secret: CUSTOMER_JWT_SECRET env var * * Token is passed as: * 1. Query param: ?token=... (email links) * 2. Authorization header: Bearer ... (XHR/fetch from portal page) * 3. Cookie: pw_portal_token=... (set by the portal page on first load) * * Helper: generatePortalToken(order_id, order_type, email) → signed JWT */ import { type Request, type Response, type NextFunction } from "express"; import jwt from "jsonwebtoken"; const CUSTOMER_JWT_SECRET = process.env.CUSTOMER_JWT_SECRET || "changeme_long_random_string"; const TOKEN_TTL_SECONDS = 72 * 60 * 60; // 72 hours export interface PortalTokenPayload { order_id: string; order_type: string; email: string; } // ─── Generate a signed portal link token ───────────────────────────────────── export function generatePortalToken( order_id: string, order_type: string, email: string, ): string { return jwt.sign( { order_id, order_type, email } satisfies PortalTokenPayload, CUSTOMER_JWT_SECRET, { expiresIn: TOKEN_TTL_SECONDS }, ); } // ─── Build a signed portal URL ──────────────────────────────────────────────── export function portalUrl( path: string, // e.g. "/portal/domain-search" order_id: string, order_type: string, email: string, ): string { const token = generatePortalToken(order_id, order_type, email); const domain = process.env.DOMAIN ? `https://${process.env.DOMAIN}` : "http://localhost:4321"; return `${domain}${path}?token=${encodeURIComponent(token)}`; } // ─── Middleware: verify portal token ───────────────────────────────────────── // Attaches req.portalAuth = { order_id, order_type, email } on success. // Returns 401 if token is missing, 403 if invalid/expired. declare global { namespace Express { interface Request { portalAuth?: PortalTokenPayload; } } } export function requirePortalAuth(req: Request, res: Response, next: NextFunction): void { // 1. Query param (email link on first load) let rawToken = (req.query.token as string) || null; // 2. Authorization header (XHR from portal page after first load) if (!rawToken) { const authHeader = req.headers.authorization || ""; if (authHeader.startsWith("Bearer ")) { rawToken = authHeader.slice(7); } } // 3. Cookie (set by portal page JS after extracting from URL) if (!rawToken) { rawToken = (req.cookies?.pw_portal_token as string) || null; } if (!rawToken) { res.status(401).json({ error: "Authentication required. Please use the link from your email.", code: "AUTH_REQUIRED" }); return; } try { const payload = jwt.verify(rawToken, CUSTOMER_JWT_SECRET) as PortalTokenPayload & { iat: number; exp: number }; req.portalAuth = { order_id: payload.order_id, order_type: payload.order_type, email: payload.email, }; next(); } catch (err: any) { if (err?.name === "TokenExpiredError") { res.status(403).json({ error: "Your portal link has expired. Please request a new one.", code: "TOKEN_EXPIRED" }); } else { res.status(403).json({ error: "Invalid portal link.", code: "TOKEN_INVALID" }); } } }