feat: add apps/api (NestJS + Fastify) with authentication endpoints
- POST /auth/login, /auth/refresh, /auth/logout, /auth/select-tenant, /auth/change-password, GET /auth/tenants — wired to packages/auth - JwtAuthGuard + DomainExceptionFilter (401/403 without leaking internals) - LoginRateLimitGuard: Redis-backed 5/min per IP and per email (agente.md secao 149), safe across multiple API instances - helmet + restrictive cors (deny-by-default) + global rate limit - GET /health, /health/live, /health/ready checking Postgres and Redis - changePassword() added to packages/auth for the mustChangePassword flow - fixed REDIS_HOST/POSTGRES_HOST docker-compose-only hostnames not resolving from the host process; added REDIS_URL for host-side use - verified end-to-end with curl: login, wrong password / unknown email (same generic error), authenticated route, missing token, refresh rotation, logout revocation, and the 429 rate limit kicking in after 5 attempts
This commit is contained in:
73
apps/api/src/auth/auth.controller.ts
Normal file
73
apps/api/src/auth/auth.controller.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Req, UseGuards } from "@nestjs/common";
|
||||
import type { FastifyRequest } from "fastify";
|
||||
import {
|
||||
changePassword,
|
||||
listUserTenants,
|
||||
login,
|
||||
logout,
|
||||
refreshSession,
|
||||
setActiveTenant,
|
||||
type AccessTokenClaims,
|
||||
} from "@b2bcall/auth";
|
||||
import { JwtAuthGuard } from "../common/guards/jwt-auth.guard";
|
||||
import { CurrentUser } from "../common/decorators/current-user.decorator";
|
||||
import { LoginDto } from "./dto/login.dto";
|
||||
import { RefreshDto } from "./dto/refresh.dto";
|
||||
import { SelectTenantDto } from "./dto/select-tenant.dto";
|
||||
import { ChangePasswordDto } from "./dto/change-password.dto";
|
||||
import { LoginRateLimitGuard } from "./login-rate-limit.guard";
|
||||
|
||||
@Controller("auth")
|
||||
export class AuthController {
|
||||
@UseGuards(LoginRateLimitGuard)
|
||||
@Post("login")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async login(@Body() dto: LoginDto, @Req() req: FastifyRequest) {
|
||||
return login({
|
||||
email: dto.email,
|
||||
password: dto.password,
|
||||
ipAddress: req.ip,
|
||||
userAgent: req.headers["user-agent"],
|
||||
});
|
||||
}
|
||||
|
||||
@Post("refresh")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async refresh(@Body() dto: RefreshDto) {
|
||||
return refreshSession(dto.refreshToken);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post("logout")
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async logout(@CurrentUser() user: AccessTokenClaims) {
|
||||
await logout(user.sessionId);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get("tenants")
|
||||
async tenants(@CurrentUser() user: AccessTokenClaims) {
|
||||
const memberships = await listUserTenants(user.sub);
|
||||
return memberships.map((m) => ({
|
||||
tenantId: m.tenant.id,
|
||||
code: m.tenant.code,
|
||||
name: m.tenant.tradeName ?? m.tenant.legalName,
|
||||
status: m.tenant.status,
|
||||
}));
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post("select-tenant")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async selectTenant(@CurrentUser() user: AccessTokenClaims, @Body() dto: SelectTenantDto) {
|
||||
const accessToken = await setActiveTenant(user.sessionId, user.sub, dto.tenantId);
|
||||
return { accessToken };
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post("change-password")
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async changePassword(@CurrentUser() user: AccessTokenClaims, @Body() dto: ChangePasswordDto) {
|
||||
await changePassword(user.sub, user.sessionId, dto.currentPassword, dto.newPassword);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user