feat(auth): e-mail transacional real + recuperação de senha

Liga o adapter SMTP existente (packages/integrations, já era fail-soft)
a um consumidor de verdade e adiciona o fluxo completo de recuperação
de senha, faltante desde a Fase 1.

- UsersService.invite() agora envia e-mail com a senha temporária
  (mantém o retorno na resposta também — dupla entrega, eden.md §4.8).
- POST /v1/auth/forgot-password + POST /v1/auth/reset-password: token
  de 256 bits, só o hash SHA-256 persistido (users.reset_token_hash/
  reset_token_expires_at — colunas que já existiam desde a baseline da
  Fase 1, nunca usadas até agora), validade de 1h, uso único, revoga
  todas as sessões ativas do usuário ao concluir. Mesma disciplina
  anti-enumeração do login: resposta idêntica para e-mail cadastrado
  ou não.
- Frontend: link "Esqueci minha senha" no LoginPage, novas
  ForgotPasswordPage/ResetPasswordPage.

Teste de integração novo (password-reset.spec.ts, 3 testes) usa
vi.mock para interceptar sendMail e recuperar o token real do corpo do
e-mail — única forma de testar o caminho feliz de ponta a ponta, já
que o token nunca é devolvido na resposta da API. Suíte completa:
33/33 (30→33). pnpm turbo run typecheck build (28/28). Testado ao vivo
através do proxy same-origin: anti-enumeração confirmada com curl
real, token gravado no Postgres, token bogus rejeitado (400).

Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 08:02:33 -03:00
parent 012c00fa4d
commit 4d23af3b23
16 changed files with 525 additions and 8 deletions

View File

@@ -430,3 +430,26 @@ Ambos os agentes reconciliados manualmente em `App.tsx`/`AuthenticatedLayout.tsx
### Estado do EDEN Core ao final desta sequência
UI real para: Empresas, Produtos (+ perfil fiscal), Revendas, CRM (Leads/Oportunidades), Ofertas, Cadastro de Cliente, Contratos, Estoque, Faturamento, Suporte, Fiscal, Documentos (fundação), Ponto Eletrônico (fundação). Cobertura completa de todo o backend construído nesta sessão, exceto Assinatura Eletrônica (deliberadamente não construída — módulo próprio, ainda não existe no backend) e apuração/fechamento de ponto (idem). `reseller-web`/`subscriber-web` seguem só com o shell de login — nenhuma tela de negócio própria ainda.
---
## E-mail transacional: convite de usuário + recuperação de senha
Data: 2026-09-04. Usuário perguntou onde configurar as credenciais de e-mail (`noreply@handix.com.br`) — confirmado que é via `.env` (`SMTP_HOST/PORT/USER/PASS/FROM`, já existiam desde a Fase 1, sem consumidor real). Pedido em seguida: ligar o envio de verdade e construir recuperação de senha na tela de login.
### Backend (`apps/api/src/auth/`, `apps/api/src/users/`) — implementado diretamente (fluxo de autenticação, não delegado)
- `@eden/integrations` adicionado como dependência real de `apps/api` (só existia como biblioteca solta até agora).
- `UsersService.invite()` agora envia e-mail de verdade com a senha temporária (mantendo o retorno na resposta da API também — dupla entrega, eden.md §4.8) via o adapter SMTP existente (`packages/integrations/src/smtp/mailer.ts`, já era fail-soft: nunca quebra o fluxo de convite se o SMTP falhar ou não estiver configurado).
- Novo fluxo de recuperação de senha: `POST /v1/auth/forgot-password` (`{email}` → sempre a mesma resposta genérica, cadastrado ou não — mesma disciplina anti-enumeração do login) e `POST /v1/auth/reset-password` (`{token, newPassword}`). Token de 256 bits, só o hash SHA-256 é persistido (`users.reset_token_hash`/`reset_token_expires_at`, colunas que já existiam desde a migration baseline da Fase 1 mas nunca tinham sido usadas), validade de 1h, uso único (limpo após consumido), e **revoga todas as sessões ativas do usuário** ao completar a redefinição (senha mudou → toda sessão anterior morre).
### Frontend (`apps/core-web`)
Link "Esqueci minha senha" no `LoginPage`, nova `ForgotPasswordPage` (mensagem genérica de sucesso, nunca confirma se o e-mail existe) e `ResetPasswordPage` (token vem da query string do link do e-mail, confirmação de senha, redireciona pro login com banner de sucesso ao concluir).
### Verificação
Teste de integração novo (`test/password-reset.spec.ts`, 3 testes) — usa `vi.mock` para interceptar `sendMail` e extrair o token real do corpo do e-mail (a única forma de testar o caminho feliz de ponta a ponta, já que o token nunca é devolvido na resposta da API, só por e-mail): confirma resposta genérica idêntica para e-mail cadastrado/não-cadastrado, token bogus rejeitado, e o ciclo completo — sessão anterior revogada, senha antiga para de funcionar, nova senha funciona, token não pode ser reaproveitado. Suíte completa: 33/33 verdes (30→33). `pnpm turbo run typecheck build` (28/28). Testado ao vivo através do proxy same-origin: anti-enumeração confirmada com curl real, token gravado no Postgres, token bogus rejeitado (400), telas novas confirmadas no bundle de produção.
### Pendências
SMTP ainda não configurado no `.env` real (aguardando credenciais do usuário) — o fluxo todo já funciona corretamente sem SMTP configurado (fail-soft), só não envia e-mail de verdade ainda. Sem rate-limiting no `forgot-password` (mesma lacuna geral já registrada para outros endpoints de auth).