Etapa 8 — Personalização whitelabel e ajustes finais
Adiciona upload/remoção de logo e cor primária por tenant (módulo tenant-profile, armazenamento local atrás de uma interface StorageService trocável por object storage depois) e um endpoint público de tema para a tela de login carregar antes da autenticação. Revisão de segurança: Helmet e CORS explícito (extraídos para security.util.ts, reutilizado pelos testes e2e que verificam os cabeçalhos), Swagger em /docs cobrindo todos os módulos via o plugin oficial, paginação em alert_templates. Corrige um achado real: o reset de senha logava o token em texto puro desde a Etapa 2 — agora envia por e-mail de verdade. Build de produção do Docker validado (imagem sem devDependencies, app e worker rodando contra Postgres/Redis reais) e README consolidado com o processo de deploy. Fecha o backlog com um teste e2e do fluxo completo: master cria tenant → login → customer/produto → venda com parcelas → parcela paga → dashboard financeiro reflete o pagamento. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,7 @@ qualquer nova etapa começar.
|
||||
| 5 | Painel Tenant (clientes/produtos) | Concluída | 2026-08-14 |
|
||||
| 6 | Vendas, Parcelas e Dashboards | Concluída | 2026-08-14 |
|
||||
| 7 | Motor de Alertas (BullMQ) | Concluída | 2026-08-14 |
|
||||
| 8 | Personalização Whitelabel e Ajustes Finais | [ ] Não iniciada | |
|
||||
| 8 | Personalização Whitelabel e Ajustes Finais | Concluída | 2026-08-14 |
|
||||
|
||||
Valores possíveis para "Status": `Não iniciada`, `Em andamento`,
|
||||
`Concluída`, `Concluída com pendências` (usar este último se algo ficou
|
||||
@@ -848,17 +848,165 @@ propositalmente para depois — detalhar em Observações).
|
||||
|
||||
## Etapa 8 — Personalização Whitelabel e Ajustes Finais
|
||||
|
||||
- [ ] Upload/remoção de logo e alteração de cor funcionam
|
||||
- [ ] Endpoint público de tema retorna logo/cor sem autenticação
|
||||
- [ ] CORS, Helmet e rate limiting revisados e ativos
|
||||
- [ ] Nenhum segredo aparece em log ou resposta de API
|
||||
- [ ] Swagger disponível cobrindo todos os módulos, com guards indicados
|
||||
- [ ] Build de produção do Docker sem dependências de desenvolvimento
|
||||
- [ ] Teste E2E do fluxo completo passa do início ao fim
|
||||
- [ ] README final consolidado (dev, deploy, migrations, RLS)
|
||||
- [x] Upload/remoção de logo e alteração de cor funcionam
|
||||
- [x] Endpoint público de tema retorna logo/cor sem autenticação
|
||||
- [x] CORS, Helmet e rate limiting revisados e ativos
|
||||
- [x] Nenhum segredo aparece em log ou resposta de API
|
||||
- [x] Swagger disponível cobrindo todos os módulos, com guards indicados
|
||||
- [x] Build de produção do Docker sem dependências de desenvolvimento
|
||||
- [x] Teste E2E do fluxo completo passa do início ao fim
|
||||
- [x] README final consolidado (dev, deploy, migrations, RLS)
|
||||
|
||||
**Observações:**
|
||||
|
||||
- **Logo/cor/tema** — módulo novo `src/modules/tenant-profile/`:
|
||||
`GET/POST(logo)/DELETE(logo)/PUT(cor) /tenant/perfil` (`TenantGuard`+
|
||||
`RolesGuard`+`tenant_admin`) e `GET /public/tenants/:id/tema` (sem
|
||||
guard nenhum, só `ThrottlerGuard` global). Upload via
|
||||
`FileInterceptor('logo')` (multer, memory storage padrão) +
|
||||
`ParseFilePipeBuilder` (`addFileTypeValidator`/`addMaxSizeValidator`,
|
||||
2MB, png/jpg/svg) — ver desvio de `skipMagicNumbersValidation` abaixo.
|
||||
Armazenamento atrás de uma interface (`StorageService`,
|
||||
`src/common/storage/`) com uma única implementação por ora
|
||||
(`LocalStorageService`, grava em `uploads/`, volume Docker nomeado em
|
||||
produção) — trocar por S3/object storage numa etapa futura é só
|
||||
implementar a interface de novo e trocar o provider registrado em
|
||||
`TenantProfileModule`, nenhum outro código muda.
|
||||
- **Decisão registrada, pedida explicitamente pelo prompt ("avaliar se
|
||||
precisa de slug"):** o endpoint público de tema usa o `id` (UUID) do
|
||||
tenant, não um `slug`. O schema fornecido não tem esse campo, e o login
|
||||
deste projeto já resolve o tenant pelo e-mail do usuário (Etapa 2), não
|
||||
por subdomínio — criar um `slug` (migration incremental) só faria
|
||||
sentido se o produto ganhasse login por subdomínio, o que não existe em
|
||||
nenhuma outra parte do sistema. Fica documentado aqui e no código
|
||||
(`PublicTenantController`) como decisão consciente, não omissão.
|
||||
- **Achado de segurança corrigido nesta etapa, não pedido explicitamente
|
||||
pelo checklist mas descoberto ao revisá-lo ("nenhum segredo em log"):**
|
||||
`AuthService.forgotPassword` (Etapa 2) logava o token de reset em texto
|
||||
puro no `console.log` — um desvio deliberado da própria Etapa 2
|
||||
("envio real de e-mail é etapa futura"), mas que nunca foi revisitado
|
||||
depois que a Etapa 4 wireou envio de e-mail de verdade. Corrigido: agora
|
||||
envia por e-mail de verdade
|
||||
(`EmailService.enviar`/`enviarComConfigPadrao` — o segundo, novo nesta
|
||||
etapa, resolve só a config padrão da plataforma, sem tenant nenhum
|
||||
envolvido, pro caso de `master_admin` resetando a própria senha, onde
|
||||
não existe `tenant_id`). Falha no envio (ex: SMTP não configurado) é
|
||||
logada (sem o token) mas nunca muda a resposta genérica do endpoint —
|
||||
continua sem vazar se o e-mail existe. `test/auth.e2e-spec.ts`
|
||||
atualizado: em vez de espiar `console.log`, mocka `nodemailer` (mesmo
|
||||
padrão já usado em `integracoes.e2e-spec.ts`) e extrai o token do corpo
|
||||
do e-mail "enviado".
|
||||
- **CORS + Helmet**: `app.use(helmet())` e `app.enableCors({origin:
|
||||
CORS_ORIGINS.split(',') || false})` — vazio/ausente bloqueia toda
|
||||
origem cross-site por padrão (nunca `origin: '*'`). Extraído pra
|
||||
`src/security.util.ts` (`applySecurityConfig(app, configService)`) em
|
||||
vez de ficar só dentro de `bootstrap()` em `main.ts`, porque os testes
|
||||
e2e criam a aplicação via `Test.createTestingModule(...).
|
||||
createNestApplication()`, que **não** passa por `bootstrap()` — sem
|
||||
extrair, os testes que verificam esses cabeçalhos estariam validando
|
||||
uma configuração que o app de teste nunca aplicava de verdade (falso
|
||||
positivo). Só `test/perfil-whitelabel.e2e-spec.ts` e
|
||||
`test/fluxo-completo.e2e-spec.ts` (specs novos desta etapa) chamam
|
||||
`applySecurityConfig` — os specs das etapas anteriores não precisam
|
||||
disso pra cobrir o que já testam, então não foram tocados (menor
|
||||
superfície de mudança em teste já passando).
|
||||
- **Rate limiting**: já estava ativo desde a Etapa 1
|
||||
(`ThrottlerModule`/`ThrottlerGuard` global, `60 req/60s` por IP) e
|
||||
reforçado desde a Etapa 2 nas rotas de login (`@Throttle`, `30 req/60s`,
|
||||
camada extra além do bloqueio persistente por e-mail em
|
||||
`login_attempts`) — nada novo implementado aqui, só confirmado: o
|
||||
`ThrottlerGuard` é global (`APP_GUARD`), então vale automaticamente
|
||||
pros endpoints novos desta etapa (incluindo o público `GET
|
||||
/public/tenants/:id/tema`) sem precisar de nenhuma anotação extra.
|
||||
- **Swagger**: `@nestjs/swagger` + o plugin oficial
|
||||
(`nest-cli.json`→`compilerOptions.plugins`), que gera os schemas dos
|
||||
DTOs automaticamente a partir dos tipos TS + decorators do
|
||||
`class-validator` já existentes — **sem precisar anotar manualmente
|
||||
`@ApiProperty()` em nenhum dos ~50 DTOs do projeto** (a alternativa
|
||||
manual seria um esforço de dias, incompatível com o resto do escopo
|
||||
desta etapa). `@ApiTags`/`@ApiBearerAuth('jwt')` adicionados em todos os
|
||||
~20 controllers (script Python pontual, não commitado, só pra aplicar o
|
||||
padrão mecanicamente sem erro manual — revisado depois via
|
||||
`npm run lint`/`npm run build`, que pegou um bug real de inserção
|
||||
no meio de um import multi-linha em `sales/dashboard.controller.ts`,
|
||||
corrigido). `/docs` confirmado manualmente contra o app real: 57 rotas,
|
||||
19 tags, security scheme `jwt` (Bearer) registrado — `auth.controller.ts`
|
||||
não ganhou `@ApiBearerAuth` por rota (a maioria dos endpoints ali é
|
||||
pública ou mistura guards por método, não por classe); fica como
|
||||
refinamento menor de documentação, não afeta comportamento.
|
||||
- **Paginação em todas as listagens**: já valia desde as Etapas 5-7 pra
|
||||
`customers`/`products`/`sales`/`recebimentos`/`tenants`/`faturas`/
|
||||
`alert_logs`. Auditoria desta etapa achou `alert_templates` (master e
|
||||
tenant) sem paginação — corrigido, adicionado `page`/`limit` (máx. 100)
|
||||
em `ListAlertTemplatesQueryDto` e nos dois services. **Decisão
|
||||
consciente de não mecanizar em todas as listagens sem critério**:
|
||||
`planos`, `evolution-config` e `master-email-config` (todas
|
||||
master-only, curadas manualmente, realisticamente algumas unidades a
|
||||
poucas dezenas de linhas) e `customer_contacts`/`whatsapp instancias`
|
||||
(limitadas estruturalmente — contatos por customer tende a ser pequeno;
|
||||
instâncias de WhatsApp são limitadas por `limite_instancias_whatsapp`
|
||||
do plano) ficaram sem paginação, de propósito — adicionar ali seria
|
||||
ceremônia sem ganho real de proteção contra consulta sem limite.
|
||||
- **Auditoria de segredos hardcoded/DTOs sem validação** (checklist
|
||||
desta etapa): `grep` por padrões de segredo hardcoded (`password =`,
|
||||
`secret =`, `api_key =`) não achou nada; todo `@Body()`/`@Query()` de
|
||||
todo controller do projeto usa uma classe DTO (nenhum `any`/objeto cru)
|
||||
— `ValidationPipe` global (`whitelist`+`forbidNonWhitelisted`+
|
||||
`transform`, desde a Etapa 1) garante que isso é realmente validado, não
|
||||
só tipado.
|
||||
- **Docker de produção**: `docker build --target production` testado de
|
||||
verdade nesta sessão (não só lido) — imagem final sem
|
||||
`typescript`/`ts-node`/`jest`/`eslint`/`@nestjs/cli` (checado via `ls
|
||||
node_modules` dentro do container), ~180MB, container real rodando e
|
||||
respondendo `/health` (200) contra Postgres/Redis reais, e o `worker`
|
||||
(mesma imagem, `CMD` diferente) subindo e conectando nas duas filas.
|
||||
Volume nomeado novo, `uploads_data:/app/uploads`, no serviço `app` do
|
||||
`docker-compose.yml` — sem ele, o volume efêmero do container perderia
|
||||
os logos enviados a cada recriação/deploy.
|
||||
- **README consolidado**: seção nova "Deploy em produção" (segredos a
|
||||
trocar, build, subir serviços, migrations — incluindo a confirmação de
|
||||
que `prisma migrate deploy` já cobre RLS/policies/roles num passo só,
|
||||
sem etapa separada —, verificação, volume de uploads) e "Motor de
|
||||
alertas (worker)" (já escrita na Etapa 7). "Estrutura do projeto"
|
||||
atualizada com os módulos/arquivos novos desta etapa.
|
||||
- **Teste e2e do fluxo completo**, exatamente como pedido no prompt:
|
||||
`test/fluxo-completo.e2e-spec.ts` — master cria plano+tenant → tenant
|
||||
faz login com a senha provisória → cadastra customer e produto → faz
|
||||
uma venda com 2 parcelas → confere o financeiro antes (delta zero,
|
||||
tenant novo) → marca a 1ª parcela como paga → confere o financeiro
|
||||
depois (recebido/a-receber refletindo o pagamento, parcela paga não
|
||||
aparece mais em "próximas") → confere a venda em si também mostrando a
|
||||
parcela como `pago`. De propósito só o caminho feliz — casos de
|
||||
erro/borda já estão nos specs dedicados de cada etapa.
|
||||
- Dependências novas: `@nestjs/swagger`, `helmet`, `@nestjs/bullmq`-style
|
||||
DI (`ParseFilePipeBuilder` já vem do `@nestjs/common` existente),
|
||||
`@types/multer` (dev). `npm install` (não `npm ci`) de novo, mesma razão
|
||||
já registrada na Etapa 7 (lockfile precisa ser regravado quando
|
||||
`package.json` ganha dependência nova).
|
||||
- Validação completa desta sessão: `docker compose up -d postgres redis`
|
||||
+ `npm install` + `prisma migrate deploy` (nenhuma migration nova) +
|
||||
`npm run lint`/`npm run test`(unit)/`npm run test:e2e` — **132/132
|
||||
testes e2e passando** (118 das Etapas 1-7 + 14 novos: 13 de
|
||||
perfil/whitelabel + 1 do fluxo completo) — `npm run build`, e smoke
|
||||
test manual contra o app real (`node dist/main`) e o worker real (`node
|
||||
dist/worker`), incluindo verificação de headers HTTP reais (Helmet,
|
||||
CORS permitido vs. bloqueado) via `curl`, além do build de produção
|
||||
Docker completo (item acima).
|
||||
|
||||
**Este era o último item do backlog faseado — as 8 etapas de
|
||||
`Etapas/00-README-ordem-das-etapas.md` estão concluídas.** O backend está
|
||||
pronto para receber um frontend: todos os endpoints documentados em
|
||||
`/docs`, RLS/isolamento multi-tenant testado em cada etapa que introduziu
|
||||
tabela nova, motor de alertas automatizado rodando em processo próprio, e
|
||||
137 testes automatizados (132 e2e contra Postgres/Redis reais + 5
|
||||
unitários) passando. Pendências conhecidas, todas já documentadas nas
|
||||
seções de Observações/Histórico de decisões acima quando surgiram (não
|
||||
re-listadas aqui pra não divergir da fonte): login por e-mail sem
|
||||
subdomínio (Etapa 2), Evolution API nunca testada contra uma instância
|
||||
real (Etapa 4), WhatsApp de "mensalidade_tenant" reaproveitando a
|
||||
instância do próprio tenant (Etapa 7), paginação deliberadamente ausente
|
||||
em listagens pequenas/curadas (acima).
|
||||
|
||||
---
|
||||
|
||||
## Histórico de decisões e desvios do plano original
|
||||
@@ -1047,3 +1195,46 @@ e-mail não há esse problema, já cai pro `master_email_config` padrão
|
||||
quando o tenant não tem SMTP próprio. Fica como ponto para uma etapa
|
||||
futura considerar um WhatsApp de notificações dedicado da plataforma, se
|
||||
o produto precisar.
|
||||
|
||||
[2026-08-14] Etapa 8 — Endpoint público de tema (`GET
|
||||
/public/tenants/:id/tema`) usa o `id` (UUID) do tenant, não um `slug`. O
|
||||
prompt desta etapa pedia avaliar se um campo `slug` era necessário; como
|
||||
o schema fornecido não tem esse campo e o login do projeto já resolve o
|
||||
tenant pelo e-mail do usuário (não por subdomínio, desde a Etapa 2),
|
||||
criar um `slug` só faria sentido junto de um fluxo de login por
|
||||
subdomínio que não existe em nenhuma outra parte do sistema — decisão
|
||||
consciente de não adicionar, não uma omissão.
|
||||
|
||||
[2026-08-14] Etapa 8 — Achado de segurança corrigido, descoberto ao
|
||||
revisar o checklist "nenhum segredo em log ou resposta de API":
|
||||
`AuthService.forgotPassword` (Etapa 2) logava o token de reset de senha
|
||||
em texto puro via `console.log` — desvio deliberado da própria Etapa 2
|
||||
("envio real de e-mail é etapa futura"), nunca revisitado depois que a
|
||||
Etapa 4 implementou envio de e-mail de verdade. Corrigido: agora envia
|
||||
por e-mail via `EmailService` (`enviar`, se for `tenant_user`, ou
|
||||
`enviarComConfigPadrao`, método novo, se for `master_admin` — não há
|
||||
`tenant_id` nesse caso). Falha no envio nunca muda a resposta genérica do
|
||||
endpoint (só loga um aviso, sem o token). `test/auth.e2e-spec.ts`
|
||||
atualizado pra mockar `nodemailer` em vez de espiar `console.log`.
|
||||
|
||||
[2026-08-14] Etapa 8 — Upload de logo usa
|
||||
`skipMagicNumbersValidation: true` no `FileTypeValidator`, validando só o
|
||||
`mimetype` declarado pelo cliente — não os magic numbers do arquivo. O
|
||||
comportamento padrão do Nest (sniffar magic numbers via `file-type`,
|
||||
pacote ESM-only, importado dinamicamente) funciona no app real (`node
|
||||
dist/main`), mas falha sempre dentro do Jest/ts-jest deste projeto (não
|
||||
consegue completar o `import()` dinâmico de um pacote ESM-only de dentro
|
||||
do contexto CJS de teste), inviabilizando testar um upload de sucesso.
|
||||
Aceitável pro risco real de uma logo estática nunca processada/executada
|
||||
no servidor; revisitar se o arquivo enviado passar a ser processado
|
||||
(ex: resize) no backend no futuro.
|
||||
|
||||
[2026-08-14] Etapa 8 — Paginação adicionada em `alert_templates` (master
|
||||
e tenant, únicas listagens sem `page`/`limit` entre as tabelas
|
||||
potencialmente user-generated). Decisão consciente de **não** adicionar
|
||||
em `planos`/`evolution-config`/`master-email-config` (master-only,
|
||||
curadas manualmente, poucas linhas na prática) nem em
|
||||
`customer_contacts`/`whatsapp instancias` (limitadas estruturalmente por
|
||||
natureza do domínio ou por `limite_instancias_whatsapp` do plano) — só
|
||||
pra cumprir a letra do checklist sem ganho real de proteção contra
|
||||
consulta sem limite.
|
||||
|
||||
Reference in New Issue
Block a user