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:
2026-08-28 06:12:34 -03:00
parent 70c5586595
commit 68b403a7ff
22 changed files with 1190 additions and 11 deletions

View File

@@ -71,18 +71,52 @@ O seed cria `admin@b2bcall.local` com senha aleatória de 24 bytes, salva uma
`mustChangePassword = true`. Rodar de novo o seed não recria o admin se já
existir um usuário com role `platform_super_admin`.
## O que falta (fica para quando existir `apps/api`)
## Camada HTTP (`apps/api`)
Tudo abaixo depende de uma camada HTTP (NestJS/Fastify) que ainda não existe:
NestJS sobre Fastify (agente.md secao 12). Rotas em `apps/api/src/auth`:
```
POST /auth/login { email, password } -> { accessToken, refreshToken, sessionId, mustChangePassword }
POST /auth/refresh { refreshToken } -> { accessToken, refreshToken }
POST /auth/logout (autenticado) -> 204
GET /auth/tenants (autenticado) -> [{ tenantId, code, name, status }]
POST /auth/select-tenant (autenticado) { tenantId } -> { accessToken }
POST /auth/change-password (autenticado) { currentPassword, newPassword } -> 204
```
- `JwtAuthGuard` (`apps/api/src/common/guards`) valida o access token e injeta
`request.user` (claims do JWT); `@CurrentUser()` expõe isso no controller.
- `DomainExceptionFilter` traduz os erros de `packages/auth`
(`InvalidCredentialsError` → 401, `InvalidRefreshTokenError` → 401,
`NotATenantMemberError` → 403) sem vazar stack trace.
- `LoginRateLimitGuard` conta tentativas de login por IP **e** por e-mail via
Redis (`INCR`+`EXPIRE`, 5/minuto — agente.md secao 149), funcionando
corretamente com múltiplas instâncias da API (ao contrário de um contador em
memória local).
- `helmet` + `cors` restritivo (`CORS_ORIGIN` via env, `false` por padrão —
nega tudo até ser configurado) + rate limit global de 300/min como defesa
extra (agente.md secao 182).
- `GET /health`, `/health/live`, `/health/ready` (agente.md secao 187) —
`ready` checa Postgres e Redis de verdade.
- API escuta só em `127.0.0.1:3000` (`API_PORT`) — nunca exposta direto,
fica atrás do nginx quando ele existir.
**Pegadinha de rede corrigida durante os testes**: `REDIS_HOST=redis` /
`POSTGRES_HOST=postgres` no `.env` são os nomes de serviço do Docker Compose —
só resolvem de dentro da rede Docker. Como `apps/api` roda direto no host
(ainda não containerizada), ela usa `REDIS_URL`/`APP_DATABASE_URL`
(`@localhost`) em vez disso. Quando `b2bcall-api` virar um serviço Docker na
mesma rede, essas URLs precisam trocar para os hostnames internos.
## O que falta
- Rate limiting de login por IP/usuário (agente.md secao 149) — plugin
`@fastify/rate-limit` ou equivalente, não implementável em `packages/auth`
isoladamente.
- Endpoints REST (`POST /auth/login`, `/auth/refresh`, `/auth/logout`,
`/auth/select-tenant`) e guards HTTP que traduzem `InvalidCredentialsError`/
`NotATenantMemberError` em 401/403.
- Password reset (link expirável por e-mail) — precisa de um provedor de
e-mail/SMTP, fora do escopo desta fase.
- Reuse detection de refresh token roubado (família de tokens) — não
implementado; a rotação simples (secao 148) já está feita, a detecção de
reuso é um hardening adicional a avaliar depois.
- Progressive blocking (backoff exponencial) no login — hoje é só janela fixa
de 5/minuto.
- Enforcement server-side de `mustChangePassword` bloqueando outras rotas
(hoje só o client precisa respeitar a flag) — revisitar quando existirem
rotas de negócio de verdade para proteger.