Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Update AuthUser type in @mosaic/shared to include workspace fields - Update AuthGuard to support both cookie-based and Bearer token authentication - Add /auth/session endpoint for session validation - Install and configure cookie-parser middleware - Update CurrentUser decorator to use shared AuthUser type - Update tests for cookie and token authentication (20 tests passing) This ensures consistent authentication handling across API and web client, with proper type safety and support for both web browsers (cookies) and API clients (Bearer tokens). Fixes #193 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
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<string, unknown>;
|
|
headers: Record<string, string | string[] | undefined>;
|
|
cookies?: Record<string, string>;
|
|
}
|
|
|
|
@Injectable()
|
|
export class AuthGuard implements CanActivate {
|
|
constructor(private readonly authService: AuthService) {}
|
|
|
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
const request = context.switchToHttp().getRequest<AuthRequest>();
|
|
|
|
// 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;
|
|
}
|
|
}
|