augmenttoolkit / server /src /nest /auth /auth-public.controller.ts
Leon4gr45's picture
Upload folder using huggingface_hub (part 5)
57a889c verified
Raw
History Blame Contribute Delete
6.87 kB
import { Body, Controller, Get, HttpCode, HttpException, Param, Post, Req, Res, UseGuards } from '@nestjs/common';
import type { Request, Response } from 'express';
import { AuthService } from './auth.service';
import { RateLimitService } from './rate-limit.service';
import { OptionalJwtGuard } from './optional-jwt.guard';
import { writeAudit, getClientIp } from '../../services/auditLog';
import type { User } from '../../types';
const WINDOW = 15 * 60 * 1000;
const LOGIN_MIN_LATENCY_MS = 350;
const FORGOT_MIN_LATENCY_MS = 350;
const GENERIC_FORGOT_RESPONSE = { ok: true };
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
/**
* Public auth endpoints (no session required) — byte-identical to the legacy
* Express route (server/src/routes/auth.ts): the same per-IP rate-limit buckets
* + limits, the constant-time login/forgot latency padding, the enumeration-safe
* forgot response, the audit writes and the JWT httpOnly cookie set/clear via
* the shared cookie service (no new token shape).
*/
@Controller('api/auth')
export class AuthPublicController {
constructor(private readonly auth: AuthService, private readonly rl: RateLimitService) {}
private limit(bucket: string, req: Request, max: number): void {
if (!this.rl.check(bucket, req.ip || 'unknown', max, WINDOW, Date.now())) {
throw new HttpException({ error: 'Too many attempts. Please try again later.' }, 429);
}
}
@Get('app-config')
@UseGuards(OptionalJwtGuard)
appConfig(@Req() req: Request) {
return this.auth.getAppConfig((req.user as User | undefined) ?? undefined);
}
@Post('demo-login')
@HttpCode(200)
demoLogin(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
const result = this.auth.demoLogin();
if (result.error) {
throw new HttpException({ error: result.error }, result.status!);
}
this.auth.setAuthCookie(res, result.token!, req);
return { token: result.token, user: result.user };
}
@Get('invite/:token')
invite(@Param('token') token: string, @Req() req: Request) {
this.limit('login', req, 10);
const result = this.auth.validateInviteToken(token);
if (result.error) {
throw new HttpException({ error: result.error }, result.status!);
}
return { valid: result.valid, max_uses: result.max_uses, used_count: result.used_count, expires_at: result.expires_at };
}
@Post('register')
@HttpCode(201)
register(@Body() body: unknown, @Req() req: Request, @Res({ passthrough: true }) res: Response) {
this.limit('login', req, 10);
const result = this.auth.registerUser(body);
if (result.error) {
throw new HttpException({ error: result.error }, result.status!);
}
writeAudit({ userId: result.auditUserId!, action: 'user.register', ip: getClientIp(req), details: result.auditDetails });
this.auth.setAuthCookie(res, result.token!, req);
return { token: result.token, user: result.user };
}
@Post('login')
@HttpCode(200)
async login(@Body() body: unknown, @Req() req: Request, @Res({ passthrough: true }) res: Response) {
this.limit('login', req, 10);
const started = Date.now();
const result = this.auth.loginUser(body);
if (result.auditAction) {
writeAudit({ userId: result.auditUserId ?? null, action: result.auditAction, ip: getClientIp(req), details: result.auditDetails });
}
const elapsed = Date.now() - started;
if (elapsed < LOGIN_MIN_LATENCY_MS) await delay(LOGIN_MIN_LATENCY_MS - elapsed);
if (result.error) {
throw new HttpException({ error: result.error }, result.status!);
}
if (result.mfa_required) {
return { mfa_required: true, mfa_token: result.mfa_token };
}
this.auth.setAuthCookie(res, result.token!, req, result.remember);
return { token: result.token, user: result.user };
}
@Post('forgot-password')
@HttpCode(200)
async forgotPassword(@Body() body: { email?: unknown }, @Req() req: Request) {
this.limit('forgot', req, 3);
const started = Date.now();
const rawEmail = typeof body?.email === 'string' ? body.email : '';
const ip = getClientIp(req);
const outcome = this.auth.requestPasswordReset(rawEmail, ip);
if (outcome.reason === 'issued' && outcome.tokenForDelivery && outcome.userEmail) {
const origin = this.auth.getAppUrl();
const url = `${origin.replace(/\/$/, '')}/reset-password?token=${encodeURIComponent(outcome.tokenForDelivery)}`;
writeAudit({ userId: outcome.userId, action: 'user.password_reset_request', ip, details: { delivered: 'pending' } });
try {
const delivery = await this.auth.sendPasswordResetEmail(outcome.userEmail, url, outcome.userId);
writeAudit({ userId: outcome.userId, action: 'user.password_reset_request', ip, details: { delivered: delivery.delivered } });
} catch {
writeAudit({ userId: outcome.userId, action: 'user.password_reset_request', ip, details: { delivered: 'failed' } });
}
} else {
writeAudit({ userId: outcome.userId, action: 'user.password_reset_request', ip, details: { reason: outcome.reason } });
}
const elapsed = Date.now() - started;
if (elapsed < FORGOT_MIN_LATENCY_MS) await delay(FORGOT_MIN_LATENCY_MS - elapsed);
return GENERIC_FORGOT_RESPONSE;
}
@Post('reset-password')
@HttpCode(200)
resetPassword(@Body() body: unknown, @Req() req: Request) {
// Per-IP brute-force guard, parity with the legacy resetLimiter (5 / 15 min on
// a dedicated bucket) — without it reset tokens could be guessed unthrottled.
this.limit('reset', req, 5);
const ip = getClientIp(req);
const result = this.auth.resetPassword(body);
if (result.error) {
writeAudit({ userId: null, action: 'user.password_reset_fail', ip, details: { reason: result.error } });
throw new HttpException({ error: result.error }, result.status!);
}
if (result.mfa_required) {
return { mfa_required: true };
}
writeAudit({ userId: result.userId ?? null, action: 'user.password_reset_success', ip });
return { success: true };
}
@Post('mfa/verify-login')
@HttpCode(200)
verifyMfaLogin(@Body() body: unknown, @Req() req: Request, @Res({ passthrough: true }) res: Response) {
this.limit('mfa', req, 5);
const result = this.auth.verifyMfaLogin(body);
if (result.error) {
throw new HttpException({ error: result.error }, result.status!);
}
writeAudit({ userId: result.auditUserId!, action: 'user.login', ip: getClientIp(req), details: { mfa: true } });
this.auth.setAuthCookie(res, result.token!, req, result.remember);
return { token: result.token, user: result.user };
}
@Post('logout')
@HttpCode(200)
logout(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
this.auth.clearAuthCookie(res, req);
return { success: true };
}
}