aboutsummaryrefslogtreecommitdiff
path: root/api/src/modules/auth/auth.controller.ts
blob: 8aec64318f6f8ac74aa9f3f0e060013be6e6241c (plain)
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);
}