import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from "@nestjs/common"; import { AuthService } from "../auth.service"; import type { AuthUser } from "@mosaic/shared"; /** * Request type with authentication context */ interface AuthRequest { user?: AuthUser; session?: Record; headers: Record; cookies?: Record; } @Injectable() export class AuthGuard implements CanActivate { 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) { // Re-throw if it's already an UnauthorizedException if (error instanceof UnauthorizedException) { throw error; } throw new UnauthorizedException("Authentication failed"); } } /** * Extract token from cookie (preferred) or Authorization header */ private extractToken(request: AuthRequest): 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 stores session token in better-auth.session_token cookie) */ private extractTokenFromCookie(request: AuthRequest): string | undefined { if (!request.cookies) { return undefined; } // BetterAuth uses 'better-auth.session_token' as the cookie name by default return request.cookies["better-auth.session_token"]; } /** * Extract token from Authorization header (Bearer token) */ private extractTokenFromHeader(request: AuthRequest): 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; } }