1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
/**
* 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);
}
|