/** * TASK-0108 — optional-auth guard for the PUBLIC submit path. * * The submit endpoint is unauthenticated BY DESIGN (anonymous public * submits are the point). But when the browser happens to hold a valid user * JWT for the form's workspace, the submission is ATTRIBUTED (`submittedBy` * + the "Submitting as " header on the page). This guard sets * `req.auth` only when a valid user JWT is present; a missing, malformed or * expired token passes through as anonymous (never a 401). API keys are * ignored — a key can never attribute a public submission. */ import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; import type { Request } from "express"; import { loadEnv } from "../../config/env.js"; import { verifyToken, TokenError } from "../../lib/jwt.js"; import type { AuthContext } from "../../common/types.js"; @Injectable() export class OptionalAuthGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const req = context.switchToHttp().getRequest(); const header = req.headers.authorization; if (!header || !header.startsWith("Bearer ")) return true; const token = header.slice(7).trim(); if (!token || token.startsWith("fk_")) return true; // API keys never attribute try { const claims = verifyToken(token, loadEnv().JWT_SECRET); if (typeof claims.sub !== "string" || claims.sub.length === 0) return true; const auth: AuthContext = { kind: "user", userId: claims.sub, workspaces: Array.isArray(claims.workspaces) ? claims.workspaces : undefined, sessionId: typeof claims.sid === "string" ? claims.sid : undefined }; (req as Request & { auth?: AuthContext }).auth = auth; } catch (err) { if (err instanceof TokenError) return true; // anonymous (invalid/expired) throw err; } return true; } }