import { Controller, All, Req, Get, UseGuards, Request, Logger } from "@nestjs/common"; import { Throttle } from "@nestjs/throttler"; import type { AuthUser, AuthSession } from "@mosaic/shared"; import { AuthService } from "./auth.service"; import { AuthGuard } from "./guards/auth.guard"; import { CurrentUser } from "./decorators/current-user.decorator"; interface RequestWithSession { user?: AuthUser; session?: { id: string; token: string; expiresAt: Date; [key: string]: unknown; }; } @Controller("auth") export class AuthController { private readonly logger = new Logger(AuthController.name); constructor(private readonly authService: AuthService) {} /** * Get current session * Returns user and session data for authenticated user */ @Get("session") @UseGuards(AuthGuard) getSession(@Request() req: RequestWithSession): AuthSession { if (!req.user || !req.session) { // This should never happen after AuthGuard, but TypeScript needs the check throw new Error("User session not found"); } return { user: req.user, session: { id: req.session.id, token: req.session.token, expiresAt: req.session.expiresAt, }, }; } /** * Get current user profile * Returns basic user information */ @Get("profile") @UseGuards(AuthGuard) getProfile(@CurrentUser() user: AuthUser): AuthUser { // Return only defined properties to maintain type safety const profile: AuthUser = { id: user.id, email: user.email, name: user.name, }; if (user.image !== undefined) { profile.image = user.image; } if (user.emailVerified !== undefined) { profile.emailVerified = user.emailVerified; } if (user.workspaceId !== undefined) { profile.workspaceId = user.workspaceId; } if (user.currentWorkspaceId !== undefined) { profile.currentWorkspaceId = user.currentWorkspaceId; } if (user.workspaceRole !== undefined) { profile.workspaceRole = user.workspaceRole; } return profile; } /** * Handle all other auth routes (sign-in, sign-up, sign-out, etc.) * Delegates to BetterAuth * * Rate limit: "strict" tier (10 req/min) - More restrictive than normal routes * to prevent brute-force attacks on auth endpoints * * Security note: This catch-all route bypasses standard guards that other routes have. * Rate limiting and logging are applied to mitigate abuse (SEC-API-10). */ @All("*") @Throttle({ strict: { limit: 10, ttl: 60000 } }) async handleAuth(@Req() req: Request): Promise { // Extract client IP for logging const clientIp = this.getClientIp(req); const requestPath = (req as unknown as { url?: string }).url ?? "unknown"; const method = (req as unknown as { method?: string }).method ?? "UNKNOWN"; // Log auth catch-all hits for monitoring and debugging this.logger.debug(`Auth catch-all: ${method} ${requestPath} from ${clientIp}`); const auth = this.authService.getAuth(); return auth.handler(req); } /** * Extract client IP from request, handling proxies */ private getClientIp(req: Request): string { const reqWithHeaders = req as unknown as { headers?: Record; ip?: string; socket?: { remoteAddress?: string }; }; // Check X-Forwarded-For header (for reverse proxy setups) const forwardedFor = reqWithHeaders.headers?.["x-forwarded-for"]; if (forwardedFor) { const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor; return ips?.split(",")[0]?.trim() ?? "unknown"; } // Fall back to direct IP return reqWithHeaders.ip ?? reqWithHeaders.socket?.remoteAddress ?? "unknown"; } }