import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, Logger, } from "@nestjs/common"; import { AuthService } from "../auth.service"; import type { AuthUser } from "@mosaic/shared"; import type { MaybeAuthenticatedRequest } from "../types/better-auth-request.interface"; @Injectable() export class AuthGuard implements CanActivate { private readonly logger = new Logger(AuthGuard.name); constructor(private readonly authService: AuthService) {} async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); // Try to get token from either cookie (preferred) or Authorization header const token = this.extractToken(request); if (!token) { throw new UnauthorizedException("No authentication token provided"); } try { const sessionData = await this.authService.verifySession(token); if (!sessionData) { throw new UnauthorizedException("Invalid or expired session"); } // Attach user and session to request const user = sessionData.user; // Validate user has required fields if (typeof user !== "object" || !("id" in user) || !("email" in user) || !("name" in user)) { throw new UnauthorizedException("Invalid user data in session"); } request.user = user as unknown as AuthUser; request.session = sessionData.session; return true; } catch (error) { if (error instanceof UnauthorizedException) { throw error; } // Infrastructure errors (DB down, connection refused, timeouts) must propagate // as 500/503 via GlobalExceptionFilter — never mask as 401 throw error; } } /** * Extract token from cookie (preferred) or Authorization header */ private extractToken(request: MaybeAuthenticatedRequest): string | undefined { // Try cookie first (BetterAuth default) const cookieToken = this.extractTokenFromCookie(request); if (cookieToken) { return cookieToken; } // Fallback to Authorization header for API clients return this.extractTokenFromHeader(request); } /** * Extract token from cookie. * BetterAuth may prefix the cookie name with "__Secure-" when running on HTTPS. */ private extractTokenFromCookie(request: MaybeAuthenticatedRequest): string | undefined { // Express types `cookies` as `any`; cast to a known shape for type safety. const cookies = request.cookies as Record | undefined; if (!cookies) { return undefined; } // BetterAuth default cookie name is "better-auth.session_token" // When Secure cookies are enabled, BetterAuth prefixes with "__Secure-". const candidates = [ "__Secure-better-auth.session_token", "better-auth.session_token", "__Host-better-auth.session_token", ] as const; for (const name of candidates) { const token = cookies[name]; if (token) { this.logger.debug(`Session cookie found: ${name}`); return token; } } return undefined; } /** * Extract token from Authorization header (Bearer token) */ private extractTokenFromHeader(request: MaybeAuthenticatedRequest): string | undefined { const authHeader = request.headers.authorization; if (typeof authHeader !== "string") { return undefined; } const parts = authHeader.split(" "); const [type, token] = parts; return type === "Bearer" ? token : undefined; } }