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:
2026-08-14 23:38:37 -03:00
co-authored by Claude Sonnet 5
parent d6d7d5fa61
commit f9738e54da
48 changed files with 1470 additions and 64 deletions
+40 -8
View File
@@ -6,6 +6,14 @@ import { AppModule } from '../src/app.module';
import { PrismaService } from '../src/config/prisma.service';
import { hashPassword } from '../src/modules/auth/password.util';
// O reset de senha (Etapa 8) manda o token por e-mail de verdade — sem SMTP
// real disponível neste ambiente, mocka `nodemailer` (mesmo padrão de
// `integracoes.e2e-spec.ts`) e extrai o token do corpo do e-mail "enviado".
const mockSendMail = jest.fn().mockResolvedValue({ messageId: 'fake-message-id' });
jest.mock('nodemailer', () => ({
createTransport: jest.fn(() => ({ sendMail: mockSendMail })),
}));
interface TokenPair {
access_token: string;
refresh_token: string;
@@ -32,6 +40,7 @@ describe('Auth (e2e)', () => {
let tenantAAdminId: string;
let tenantAVendedorId: string;
let tenantBAdminId: string;
const masterEmailConfigIds: string[] = [];
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
@@ -115,6 +124,7 @@ describe('Auth (e2e)', () => {
await prisma.login_attempts.deleteMany({ where: { email: { in: emails } } });
await prisma.tenant_users.deleteMany({ where: { tenant_id: { in: [tenantAId, tenantBId] } } });
await prisma.tenants.deleteMany({ where: { id: { in: [tenantAId, tenantBId] } } });
await prisma.master_email_config.deleteMany({ where: { id: { in: masterEmailConfigIds } } });
await prisma.master_admins.deleteMany({ where: { id: masterId } });
await app.close();
@@ -249,21 +259,43 @@ describe('Auth (e2e)', () => {
});
describe('esqueci minha senha / reset', () => {
beforeAll(async () => {
// Reset de senha manda o token por e-mail de verdade (Etapa 8) — sem
// config de e-mail padrão ativa, `forgotPassword` engoliria o erro de
// envio (nunca deixa a falta de SMTP vazar informação pela resposta
// genérica) e nenhum e-mail chegaria a ser "enviado" pro mock capturar.
const masterLogin = await request(httpServer)
.post('/auth/master/login')
.send({ email: masterEmail, senha })
.expect(200);
const masterToken = (masterLogin.body as TokenPair).access_token;
const config = await request(httpServer)
.post('/master/email-config')
.set('Authorization', `Bearer ${masterToken}`)
.send({
smtp_host: 'smtp.exemplo.com',
smtp_porta: 587,
smtp_usuario: 'no-reply',
smtp_senha: 'senha-smtp-secreta',
email_remetente: 'no-reply@seconectai.com.br',
})
.expect(201);
masterEmailConfigIds.push((config.body as { id: string }).id);
});
it('gera token de uso único, com expiração, e permite redefinir a senha', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
mockSendMail.mockClear();
await request(httpServer)
.post('/auth/password/forgot')
.send({ email: tenantAVendedorEmail, usuario_tipo: 'tenant_user' })
.expect(200);
const logLine = consoleSpy.mock.calls
.map((args) => String(args[0]))
.find((line) => line.includes('token='));
consoleSpy.mockRestore();
expect(logLine).toBeDefined();
const token = /token=(\S+)/.exec(logLine ?? '')?.[1];
expect(mockSendMail).toHaveBeenCalledTimes(1);
const [emailEnviado] = mockSendMail.mock.calls[0] as [{ html: string }];
const corpoEmail = String(emailEnviado.html);
const token = /(?<![\w-])[\w-]{20,}(?![\w-])/.exec(corpoEmail)?.[0];
expect(token).toBeTruthy();
const novaSenha = 'NovaSenhaForte#26';