/** * auth.controller.ts * Lógica de negócio de autenticação. */ import bcrypt from 'bcryptjs'; import type { FastifyReply, FastifyRequest } from 'fastify'; import { findUserByEmail, findUserById, createUser, } from './auth.repository.js'; import type { RegisterBody, LoginBody } from './auth.schema.js'; const SALT_ROUNDS = 12; // ─── POST /auth/register ────────────────────────────────────────────────────── export async function registerHandler( request: FastifyRequest<{ Body: RegisterBody }>, reply: FastifyReply, ) { const { name, email, password, role } = request.body; const db = request.server.db; // Verifica duplicidade de e-mail const existing = await findUserByEmail(db, email); if (existing) { return reply.conflict('Este e-mail já está cadastrado.'); } // Apenas admins podem criar agentes ou outros admins if ((role === 'agent' || role === 'admin')) { const callerUser = request.user as { role?: string } | undefined; if (!callerUser || callerUser.role !== 'admin') { return reply.forbidden('Apenas administradores podem criar agentes ou admins.'); } } // Hash da senha (bcrypt com salt 12 ≈ ~250ms — seguro e aceitável) const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); const user = await createUser(db, { name, email, password: hashedPassword, role, }); // Assina o JWT com o payload mínimo necessário const token = await reply.jwtSign({ id: user.id, email: user.email, role: user.role, }); return reply.code(201).send({ token, user }); } // ─── POST /auth/login ───────────────────────────────────────────────────────── export async function loginHandler( request: FastifyRequest<{ Body: LoginBody }>, reply: FastifyReply, ) { const { email, password } = request.body; const db = request.server.db; const user = await findUserByEmail(db, email); // Mensagem genérica: não revela se o e-mail existe ou não (segurança) const INVALID_MSG = 'E-mail ou senha incorretos.'; if (!user) { // Executa bcrypt mesmo sem usuário para evitar timing attacks await bcrypt.compare(password, '$2b$12$invalidhashplaceholderthatnevermatchesanypassword'); return reply.unauthorized(INVALID_MSG); } if (!user.isActive) { return reply.forbidden('Conta desativada. Entre em contato com o administrador.'); } const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { return reply.unauthorized(INVALID_MSG); } const token = await reply.jwtSign({ id: user.id, email: user.email, role: user.role, }); // Retorna o user sem o campo password const { password: _pwd, ...safeUser } = user; return reply.send({ token, user: safeUser }); } // ─── GET /auth/me ───────────────────────────────────────────────────────────── export async function meHandler( request: FastifyRequest, reply: FastifyReply, ) { // request.user foi injetado pelo decorator authenticate const user = await findUserById(request.server.db, request.user.id); if (!user) { return reply.unauthorized('Usuário não encontrado.'); } return reply.send(user); }