aboutsummaryrefslogtreecommitdiff
path: root/api/src/modules/auth
diff options
context:
space:
mode:
authorzwlucas <lucas.fariamo08@gmail.com>2026-07-25 21:23:29 +0000
committerzwlucas <lucas.fariamo08@gmail.com>2026-07-25 21:23:29 +0000
commitef55d9bb24430610b60c1b77821307cd72c08c55 (patch)
treee6935a810b34370b0a4c821e55f6397aac50c832 /api/src/modules/auth
downloadhelpdesk-master.tar.gz
helpdesk-master.zip
feat: upload of the projectHEADmaster
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'api/src/modules/auth')
-rw-r--r--api/src/modules/auth/auth.controller.ts114
-rw-r--r--api/src/modules/auth/auth.repository.ts68
-rw-r--r--api/src/modules/auth/auth.routes.ts55
-rw-r--r--api/src/modules/auth/auth.schema.ts66
4 files changed, 303 insertions, 0 deletions
diff --git a/api/src/modules/auth/auth.controller.ts b/api/src/modules/auth/auth.controller.ts
new file mode 100644
index 0000000..8aec643
--- /dev/null
+++ b/api/src/modules/auth/auth.controller.ts
@@ -0,0 +1,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);
+}
diff --git a/api/src/modules/auth/auth.repository.ts b/api/src/modules/auth/auth.repository.ts
new file mode 100644
index 0000000..25a931b
--- /dev/null
+++ b/api/src/modules/auth/auth.repository.ts
@@ -0,0 +1,68 @@
+/**
+ * auth.repository.ts
+ * Queries de banco exclusivas do módulo de autenticação.
+ */
+
+import { eq } from 'drizzle-orm';
+import type { Database } from '@/db/connection.js';
+import { users, type NewUser } from '@/db/schema.js';
+
+// ─── Busca com senha (apenas para verificação no login) ───────────────────────
+
+export async function findUserByEmail(db: Database, email: string) {
+ const [user] = await db
+ .select()
+ .from(users)
+ .where(eq(users.email, email))
+ .limit(1);
+
+ return user ?? null;
+}
+
+// ─── Busca segura (sem password) ──────────────────────────────────────────────
+
+export async function findUserById(db: Database, id: number) {
+ const [user] = await db
+ .select({
+ id: users.id,
+ name: users.name,
+ email: users.email,
+ role: users.role,
+ isActive: users.isActive,
+ createdAt: users.createdAt,
+ updatedAt: users.updatedAt,
+ })
+ .from(users)
+ .where(eq(users.id, id))
+ .limit(1);
+
+ return user ?? null;
+}
+
+// ─── Criação de usuário ───────────────────────────────────────────────────────
+
+export async function createUser(
+ db: Database,
+ data: Pick<NewUser, 'name' | 'email' | 'password' | 'role'>,
+) {
+ const now = new Date().toISOString();
+
+ const [user] = await db
+ .insert(users)
+ .values({
+ ...data,
+ isActive: true,
+ createdAt: now,
+ updatedAt: now,
+ })
+ .returning({
+ id: users.id,
+ name: users.name,
+ email: users.email,
+ role: users.role,
+ isActive: users.isActive,
+ createdAt: users.createdAt,
+ });
+
+ return user!;
+}
diff --git a/api/src/modules/auth/auth.routes.ts b/api/src/modules/auth/auth.routes.ts
new file mode 100644
index 0000000..1de0e16
--- /dev/null
+++ b/api/src/modules/auth/auth.routes.ts
@@ -0,0 +1,55 @@
+/**
+ * auth.routes.ts
+ *
+ * Rotas públicas e protegidas de autenticação:
+ * POST /api/v1/auth/register → cria conta
+ * POST /api/v1/auth/login → retorna JWT
+ * GET /api/v1/auth/me → dados do usuário logado (protegida)
+ */
+
+import type { FastifyInstance } from 'fastify';
+import {
+ registerBodySchema,
+ loginBodySchema,
+} from './auth.schema.js';
+import {
+ registerHandler,
+ loginHandler,
+ meHandler,
+} from './auth.controller.js';
+
+async function authRoutes(fastify: FastifyInstance): Promise<void> {
+
+ // ── POST /auth/register ────────────────────────────────────────────────────
+ fastify.post('/register', {
+ schema: {
+ tags: ['Auth'],
+ summary: 'Registra um novo usuário',
+ body: registerBodySchema,
+ },
+ handler: registerHandler,
+ });
+
+ // ── POST /auth/login ───────────────────────────────────────────────────────
+ fastify.post('/login', {
+ schema: {
+ tags: ['Auth'],
+ summary: 'Autentica e retorna um JWT',
+ body: loginBodySchema,
+ },
+ handler: loginHandler,
+ });
+
+ // ── GET /auth/me ───────────────────────────────────────────────────────────
+ fastify.get('/me', {
+ schema: {
+ tags: ['Auth'],
+ summary: 'Retorna o usuário autenticado',
+ security: [{ bearerAuth: [] }],
+ },
+ preHandler: [fastify.authenticate],
+ handler: meHandler,
+ });
+}
+
+export default authRoutes;
diff --git a/api/src/modules/auth/auth.schema.ts b/api/src/modules/auth/auth.schema.ts
new file mode 100644
index 0000000..7c9e8cd
--- /dev/null
+++ b/api/src/modules/auth/auth.schema.ts
@@ -0,0 +1,66 @@
+/**
+ * auth.schema.ts
+ * Schemas Zod para registro, login e resposta de usuário.
+ */
+
+import { z } from 'zod';
+import { USER_ROLES } from '@/db/schema.js';
+
+// ─── Registro ─────────────────────────────────────────────────────────────────
+
+export const registerBodySchema = z.object({
+ name: z
+ .string({ required_error: 'Nome é obrigatório' })
+ .min(2, 'Nome deve ter no mínimo 2 caracteres')
+ .max(100)
+ .trim(),
+
+ email: z
+ .string({ required_error: 'E-mail é obrigatório' })
+ .email('E-mail inválido')
+ .toLowerCase()
+ .trim(),
+
+ password: z
+ .string({ required_error: 'Senha é obrigatória' })
+ .min(8, 'Senha deve ter no mínimo 8 caracteres')
+ .max(100),
+
+ // Somente admins podem criar agentes/admins — validado no controller
+ role: z.enum(USER_ROLES).default('requester'),
+});
+
+export type RegisterBody = z.infer<typeof registerBodySchema>;
+
+// ─── Login ────────────────────────────────────────────────────────────────────
+
+export const loginBodySchema = z.object({
+ email: z
+ .string({ required_error: 'E-mail é obrigatório' })
+ .email('E-mail inválido')
+ .toLowerCase()
+ .trim(),
+
+ password: z.string({ required_error: 'Senha é obrigatória' }),
+});
+
+export type LoginBody = z.infer<typeof loginBodySchema>;
+
+// ─── Shape de resposta (NUNCA inclui password) ────────────────────────────────
+
+export const userResponseSchema = z.object({
+ id: z.number(),
+ name: z.string(),
+ email: z.string(),
+ role: z.enum(USER_ROLES),
+ isActive: z.boolean(),
+ createdAt: z.string(),
+});
+
+export const authResponseSchema = z.object({
+ token: z.string(),
+ user: userResponseSchema,
+});
+
+export type UserResponse = z.infer<typeof userResponseSchema>;
+export type AuthResponse = z.infer<typeof authResponseSchema>;