- Apply restrictive rate limits (10 req/min) to prevent brute-force attacks - Log requests with path and client IP for monitoring and debugging - Extract client IP handling for proxy setups (X-Forwarded-For) - Add comprehensive tests for rate limiting and logging behavior Refs #338 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
125 lines
3.7 KiB
TypeScript
125 lines
3.7 KiB
TypeScript
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<unknown> {
|
|
// 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<string, string | string[] | undefined>;
|
|
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";
|
|
}
|
|
}
|