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:
+40
-8
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user