import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import { verifyToken } from "@clerk/clerk-sdk-node"; import { AuthService } from "./auth.service.js"; import { IS_PUBLIC_KEY } from "./public.decorator.js"; import { DEMO_CLERK_USER_ID, DEMO_USER_ID } from "./demo-user.js"; @Injectable() export class ClerkAuthGuard implements CanActivate { constructor( private readonly authService: AuthService, private readonly reflector: Reflector, ) {} async canActivate(context: ExecutionContext): Promise { // Skip auth for routes marked with @Public() const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ context.getHandler(), context.getClass(), ]); if (isPublic) return true; const request = context.switchToHttp().getRequest<{ headers: { authorization?: string }; userId?: string; clerkUserId?: string; }>(); const authHeader = request.headers.authorization; if (!authHeader?.startsWith("Bearer ")) { // Demo mode fallback — allow through with a demo user context // This lets local dev work without Clerk keys if (process.env.DEMO_MODE === "true") { (request as any).clerkUserId = DEMO_CLERK_USER_ID; (request as any).userId = DEMO_USER_ID; return true; } throw new UnauthorizedException("Missing Authorization header"); } const token = authHeader.replace("Bearer ", ""); try { const verifyOptions: { jwtKey?: string; secretKey?: string; authorizedParties?: string[]; } = {}; if (process.env.CLERK_JWT_KEY) { verifyOptions.jwtKey = process.env.CLERK_JWT_KEY; } if (process.env.CLERK_SECRET_KEY) { verifyOptions.secretKey = process.env.CLERK_SECRET_KEY; } if (process.env.CLERK_AUTHORIZED_PARTIES) { verifyOptions.authorizedParties = process.env.CLERK_AUTHORIZED_PARTIES.split(","); } const verified = await verifyToken(token, verifyOptions); const clerkUserId = verified.sub; // Find or create the user in our DB const user = await this.authService.findOrCreateUser({ id: clerkUserId, emailAddresses: [{ emailAddress: (verified as any).email ?? (verified as any).emailAddress ?? "" }], firstName: (verified as any).first_name ?? null, lastName: (verified as any).last_name ?? null, imageUrl: (verified as any).image_url ?? null, }); // Attach to request for controllers (request as any).clerkUserId = clerkUserId; (request as any).userId = user.id; return true; } catch (err) { console.error("Clerk Token Verification Failed:", err); if (process.env.DEMO_MODE === "true") { return this.allowDemoToken(request, token); } throw new UnauthorizedException("Invalid or expired token"); } } private async allowDemoToken( request: { userId?: string; clerkUserId?: string; }, token: string, ): Promise { try { const payload = JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString()) as { sub?: string; exp?: number; iss?: string; email?: string; emailAddress?: string; first_name?: string; last_name?: string; image_url?: string; }; const isFutureToken = typeof payload.exp === "number" && payload.exp * 1000 > Date.now(); const isClerkToken = typeof payload.iss === "string" && payload.iss.includes("clerk"); const clerkUserId = payload.sub; if (!clerkUserId || !isFutureToken || !isClerkToken) { throw new Error("Token payload failed demo checks"); } const user = await this.authService.findOrCreateUser({ id: clerkUserId, emailAddresses: [{ emailAddress: payload.email ?? payload.emailAddress ?? "" }], firstName: payload.first_name ?? null, lastName: payload.last_name ?? null, imageUrl: payload.image_url ?? null, }); request.clerkUserId = clerkUserId; request.userId = user.id; return true; } catch { throw new UnauthorizedException("Invalid or expired token"); } } }