Files
tiagoandClaude Sonnet 5 f9738e54da 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>
2026-08-14 23:38:37 -03:00

13 KiB

SeConectai Vendas

CRM/controle de vendas multi-tenant. Um painel master (dono da plataforma) administra tenants (clientes), que por sua vez cadastram seus próprios clientes ("customers"), produtos/serviços e vendas.

Stack: Node.js 20 + TypeScript, NestJS, PostgreSQL 16 + Prisma, Redis, Docker/Docker Compose. Multi-tenancy via banco único, isolado por tenant_id + Row Level Security (RLS) do Postgres.

Subindo o ambiente do zero

  1. Copie o arquivo de variáveis de ambiente e ajuste se necessário:

    cp .env.example .env
    
  2. Suba os containers (Postgres, Redis e a API com hot-reload em modo dev):

    docker compose up
    

    Isso builda a imagem app (stage dev), sobe postgres e redis com healthcheck, e só inicia a API depois que as duas dependências estiverem saudáveis. docker-compose.override.yml é aplicado automaticamente e monta o código-fonte local no container, então alterações em src/ reiniciam a API sozinhas (npm run dev / nest start --watch). O mesmo docker compose up já sobe o serviço worker (motor de alertas — ver seção própria abaixo) lado a lado com a API.

  3. Rode as migrations do Prisma (tabelas, enums, índices) e a migration manual de RLS/views/checks:

    docker compose exec app npx prisma migrate deploy
    

    Em desenvolvimento, para criar uma nova migration a partir de mudanças no schema.prisma:

    docker compose exec app npx prisma migrate dev
    
  4. Confirme que a API está de pé:

    curl http://localhost:3000/health
    

    Deve retornar 200 com {"status":"ok","database":"up",...}.

Rodando sem Docker (opcional)

Requer Node.js 20+, um Postgres 16 e um Redis acessíveis localmente.

npm install
npm run prisma:generate
npm run prisma:migrate
npm run dev

Deploy em produção

  1. Segredos: copie .env.example para .env (ou injete as mesmas variáveis via secrets do orquestrador — nunca commite o .env) e troque todos os valores marcados como "changeme"/exemplo: POSTGRES_PASSWORD, as senhas de DATABASE_URL_MASTER/ DATABASE_URL_TENANT (roles app_master/app_tenant — troque com ALTER ROLE ... WITH PASSWORD '...' no Postgres e atualize as duas connection strings de acordo), JWT_SECRET e ENCRYPTION_KEY (mínimo 32 caracteres cada). Trocar ENCRYPTION_KEY depois que já existem segredos cifrados no banco (API keys, senha SMTP) invalida todos eles — só troque nessa hora se também for recriá-los. Configure CORS_ORIGINS com a(s) origem(ns) real(is) do frontend (nunca deixe vazio em produção se o frontend for acessado via browser).

  2. Build: docker compose build (sem docker-compose.override.yml, que só se aplica automaticamente quando presente/em dev) builda a imagem production — multi-stage, sem devDependencies (prod-deps usa npm ci --omit=dev) — para os serviços app e worker.

  3. Subir os serviços: docker compose up -d — sobe postgres/redis (com healthcheck), e só inicia app/worker depois que as duas dependências estiverem saudáveis (depends_on: condition: service_healthy). app expõe HTTP (PORT, default 3000) e tem healthcheck próprio batendo em /health; worker não expõe porta (sem HTTP) e roda os crons/filas do motor de alertas — ver seção própria abaixo. Os dois escalam de forma independente (docker compose up -d --scale worker=3, por exemplo) sem duplicar o disparo dos crons.

  4. Migrations, incluindo RLS: docker compose exec app npx prisma migrate deploy aplica todas as migrations pendentes em ordem — as geradas automaticamente pelo Prisma e as manuais (RLS/policies/views, roles app_master/app_tenant, security_invoker das views de dashboard) são só pastas em prisma/migrations/, então não existe um passo separado "aplicar RLS": um único migrate deploy já cobre tudo, nessa mesma ordem, em qualquer ambiente novo.

  5. Verificação: curl https://<host>/health (200, {"status":"ok","database":"up"}), https://<host>/docs (Swagger, todos os módulos documentados com o guard exigido por cada rota) e confira nos logs do worker que as duas filas (alertas-mensalidade/ alertas-cobranca) inicializaram.

  6. Uploads de logo: docker-compose.yml já monta um volume nomeado (uploads_data:/app/uploads) pro serviço app, então logos enviados via POST /tenant/perfil/logo sobrevivem a recriações do container. Trocar por object storage (S3 etc) numa etapa futura é só implementar StorageService (src/common/storage/storage.service.ts) de novo e trocar o provider registrado em TenantProfileModule — nenhum outro código muda.

Variáveis de ambiente

Veja .env.example — todas as chaves usadas pela aplicação e pelo docker-compose.yml (credenciais do Postgres, DATABASE_URL e variantes, Redis, JWT_SECRET e demais parâmetros de autenticação) estão documentadas lá.

Autenticação

src/modules/auth/ implementa login/refresh/logout/reset de senha para os dois tipos de usuário:

  • POST /auth/master/login e POST /auth/tenant/login — e-mail+senha → access token (JWT, 15 min por padrão) + refresh token opaco (rotacionado a cada uso, guardado só como hash sha256 em auth_refresh_tokens).
  • POST /auth/refresh — rotaciona o refresh token; reuso de um token já rotacionado é tratado como indício de roubo e revoga toda a família de tokens do usuário.
  • POST /auth/logout — revoga o refresh token informado.
  • POST /auth/password/forgot / POST /auth/password/reset — gera token de reset (hash, expiração curta, uso único); por ora o token é só logado no console (envio de e-mail real é da Etapa 4).
  • Senhas com argon2id (parâmetros OWASP 2024). Bloqueio de força bruta por e-mail após LOGIN_MAX_ATTEMPTS falhas em LOGIN_LOCKOUT_WINDOW_MINUTES (tabela login_attempts), mais um rate limit por IP nas rotas de login (@nestjs/throttler).
  • JwtAuthGuard, MasterGuard, TenantGuard, RolesGuard (+ @Roles(...)) e o decorator @CurrentUser() — ver GET /auth/me, GET /auth/master/admins e GET /auth/tenant/users em auth.controller.ts para exemplos de uso de cada guard.

Migrations e RLS

O schema em prisma/schema.prisma é a tradução 1:1 de Etapas/seconectai-vendas-schema.sql (fonte de verdade). Construtos que o Prisma DSL não expressa nativamente — views de dashboard, CHECK constraints e as CREATE POLICY de Row Level Security — vivem numa migration manual em prisma/migrations/<timestamp>_rls_views_constraints/, aplicada junto com as migrations geradas automaticamente pelo Prisma (prisma migrate deploy roda todas em ordem).

Uma segunda migration manual (prisma/migrations/20260813120000_auth_app_roles/) cria duas roles de banco usadas em runtime:

  • app_master (LOGIN + BYPASSRLS, conectada via DATABASE_URL_MASTER) — usada pelo painel master, que precisa enxergar dados de todos os tenants.
  • app_tenant (LOGIN comum, sem BYPASSRLS, conectada via DATABASE_URL_TENANT) — usada por toda query autenticada como tenant_user.

Isso é necessário porque a role de DATABASE_URL (seconectai, dona das tabelas) é a superuser bootstrap do Postgres criada pelo POSTGRES_USER do docker-compose — e superusers sempre ignoram Row Level Security, mesmo com as policies criadas. Sem uma role não-superuser dedicada, o isolamento por tenant seria só decorativo.

Por request, o TenantRlsInterceptor (registrado globalmente) identifica requests autenticadas como tenant_user e, via TenantContextService, abre uma transação na conexão app_tenant executando:

SELECT set_config('app.current_tenant_id', '<uuid-do-tenant-logado>', true); -- equivalente a SET LOCAL

sempre usando o tenant_id do JWT da sessão — nunca um valor vindo de input do usuário (body/query). Serviços que precisam rodar dentro desse contexto usam TenantContextService.getTx() (ver AuthController.listTenantUsers como exemplo). Requests master usam PrismaMasterService (conexão app_master, sem passar pelas policies).

Motor de alertas (worker)

O disparo automatizado de alertas (mensalidade do tenant, cobrança de parcelas do customer) roda num processo separado do processo web:

  • src/main.ts (processo app) — só HTTP. Não importa ScheduleModule nem processa nenhuma fila.
  • src/worker.ts (processo worker) — sem HTTP. Sobe via NestFactory.createApplicationContext(WorkerModule), que inicializa os dois crons diários (AlertSchedulerService, @nestjs/schedule) e os dois processors do BullMQ (MensalidadeProcessor/CobrancaProcessor, filas alertas-mensalidade/alertas-cobranca no Redis já usado pelo docker-compose).

Essa separação existe para poder escalar os dois processos de forma independente sem duplicar o disparo do cron: se o app rodar em várias réplicas (comum atrás de um load balancer), um @Cron() registrado nele dispararia o mesmo job uma vez por réplica. Como os crons vivem só no worker, escalar worker (docker compose up --scale worker=3) paraleliza o processamento das filas, não a geração dos jobs.

docker compose up já sobe os dois processos (serviço worker no docker-compose.yml/docker-compose.override.yml, mesma imagem do app, command diferente: node dist/worker em produção, npm run worker:dev em desenvolvimento). Para rodar só o worker manualmente:

docker compose up worker

Horário dos crons, antecedência do alerta e política de retry são configuráveis via ALERT_CRON_MENSALIDADE/ALERT_CRON_COBRANCA, ALERT_DIAS_ANTECEDENCIA_MENSALIDADE/ALERT_DIAS_ANTECEDENCIA_COBRANCA e ALERT_JOB_MAX_ATTEMPTS/ALERT_JOB_BACKOFF_MS (ver .env.example).

Scripts

Script Descrição
npm run dev Inicia a API em modo watch
npm run build Compila TypeScript para dist/
npm run start:prod Roda a build de produção (dist/main.js)
npm run worker:dev Inicia o worker de alertas em modo watch
npm run worker:prod Roda a build de produção do worker (dist/worker.js)
npm run lint ESLint (com --fix)
npm run test Testes unitários (Jest)
npm run test:e2e Testes end-to-end (Jest + Supertest)
npm run prisma:generate Gera o Prisma Client
npm run prisma:migrate Cria/aplica migration em desenvolvimento
npm run prisma:migrate:deploy Aplica migrations pendentes (produção/CI)
npm run prisma:studio Abre o Prisma Studio

Estrutura do projeto

src/
├── main.ts             # processo web (HTTP)
├── worker.ts           # processo worker (crons + filas BullMQ, sem HTTP)
├── app.module.ts        # módulos do processo web
├── security.util.ts      # Helmet/CORS/arquivos estáticos — chamado por main.ts e por
│                        # testes e2e que verificam esses cabeçalhos
├── common/storage/       # StorageService (interface) + LocalStorageService (Etapa 8)
├── config/              # ConfigModule, validação de env, PrismaService/PrismaMasterService/
│                        # PrismaTenantService, TenantContextService (mecanismo de RLS por request)
├── worker/              # WorkerModule, AlertSchedulerService (crons), processors do BullMQ
└── modules/
    ├── health/          # GET /health
    ├── auth/            # login/refresh/logout/reset, guards, decorators, JwtStrategy
    ├── tenant-profile/   # GET/PUT/POST/DELETE /tenant/perfil (logo/cor), GET /public/tenants/:id/tema
    └── alerts/           # CRUD de alert_templates, GET .../alertas/logs, resolver de fallback
                          # e placeholders (usado pelo módulo web e pelo worker)
prisma/
├── schema.prisma
└── migrations/
Etapas/                  # Prompts e controle de progresso do build faseado

Documentação interativa de todos os endpoints (Swagger/OpenAPI, gerado via @nestjs/swagger + o plugin do nest-cli.json — não precisa de @ApiProperty() manual em cada DTO): /docs (/docs-json pro spec bruto).

Progresso do build

Este projeto é construído em etapas. O estado oficial de cada etapa (o que já foi concluído, o que falta, decisões tomadas) fica em Etapas/09-controle-de-progresso.md.