Explorar el Código

ajustes controllers

leonardo hace 2 meses
padre
commit
e21f2dc5f8

+ 15 - 0
src/config/index.js

@@ -52,3 +52,18 @@ export const config = {
     perMinute: Number(process.env.RATE_LIMIT_PER_MINUTE ?? 0)
   }
 };
+
+function validateConfig(cfg) {
+  const dbRequired = { host: "DB_HOST", user: "DB_USER", password: "DB_PASS", schema: "DB_SCHEMA" };
+  for (const [key, envName] of Object.entries(dbRequired)) {
+    if (!cfg.db[key]) throw new Error(`[config] ${envName} é obrigatório`);
+  }
+  if (cfg.auth.mode === "jwt" && !cfg.jwt.secret) {
+    throw new Error("[config] JWT_SECRET não pode ser vazio quando AUTH_MODE=jwt");
+  }
+  if (cfg.rateLimit.perMinute === 0) {
+    console.warn("[config] RATE_LIMIT_PER_MINUTE=0 — rate limiting desabilitado");
+  }
+}
+
+validateConfig(config);

+ 11 - 21
src/controllers/Auth.Controller.js

@@ -1,4 +1,5 @@
 import bcrypt from "bcryptjs";
+import { AppError } from "../shared/errors/index.js";
 import { Usuario } from "../models/Usuario.model.js";
 import { RefreshToken } from "../models/RefreshToken.model.js";
 import { signAccessToken, generateRefreshToken, hashRefreshToken } from "../services/authTokens.js";
@@ -31,20 +32,13 @@ function isLoginBlocked(login) {
 export const AuthController = {
   Login: async function (req, res, next) {
     try {
-      const { Login, login, Senha, senha } = req.body ?? {};
+      const { login, senha } = req.body;
 
-      const loginBody = Login ?? login;
-      const senhaBody = Senha ?? senha;
-
-      if (!loginBody || !senhaBody) {
-        return res.status(401).send({ status: false, msg: "Informacoes faltando para Login!" });
-      }
-
-      const loginInformado = String(loginBody).trim();
-      const senhaInformada = String(senhaBody);
+      const loginInformado = login.trim();
+      const senhaInformada = senha;
 
       if (isLoginBlocked(loginInformado)) {
-        return res.status(429).send({ status: false, msg: "Muitas tentativas incorretas. Tente novamente em 15 minutos." });
+        throw new AppError("Muitas tentativas incorretas. Tente novamente em 15 minutos.", 429);
       }
 
       const usuario = await Usuario.query().findOne(
@@ -53,17 +47,17 @@ export const AuthController = {
 
       if (!usuario) {
         recordFailedAttempt(loginInformado);
-        return res.status(401).send({ status: false, msg: "Combinação de usuário e senha inválida!" });
+        throw new AppError("Combinação de usuário e senha inválida!", 401);
       }
 
       if (String(usuario.Status) === "0") {
-        return res.status(401).send({ status: false, msg: "Usuario inativo!" });
+        throw new AppError("Usuario inativo!", 401);
       }
 
       const passwordIsValid = bcrypt.compareSync(senhaInformada, String(usuario.Senha ?? ""));
       if (!passwordIsValid) {
         recordFailedAttempt(loginInformado);
-        return res.status(401).send({ status: false, msg: "Combinação de usuário e senha inválida!" });
+        throw new AppError("Combinação de usuário e senha inválida!", 401);
       }
 
       _failedAttempts.delete(loginInformado);
@@ -96,11 +90,7 @@ export const AuthController = {
 
   Refresh: async function (req, res, next) {
     try {
-      const { refreshToken } = req.body ?? {};
-      if (!refreshToken) {
-        return res.status(401).send({ status: false, msg: "Refresh token ausente!" });
-      }
-
+      const { refreshToken } = req.body;
       const tokenHash = hashRefreshToken(refreshToken);
       const stored = await RefreshToken.query()
         .findOne({ TokenHash: tokenHash })
@@ -108,12 +98,12 @@ export const AuthController = {
         .where("ExpiresAt", ">", new Date());
 
       if (!stored) {
-        return res.status(401).send({ status: false, msg: "Refresh token inválido ou expirado!" });
+        throw new AppError("Refresh token inválido ou expirado!", 401);
       }
 
       const usuario = await Usuario.query().findById(stored.UsuarioId);
       if (!usuario || String(usuario.Status) === "0") {
-        return res.status(401).send({ status: false, msg: "Usuario inativo!" });
+        throw new AppError("Usuario inativo!", 401);
       }
 
       const accessToken = signAccessToken({ sub: usuario.Id, login: usuario.Login, nivel: usuario.Nivel });

+ 18 - 25
src/controllers/Chat.Controller.js

@@ -1,6 +1,20 @@
 import { answerWithContext, answerWithContextStream } from "../chat/chatChain.js";
 import { addMessage, verifyConversationOwner } from "../services/conversationsService.js";
 
+async function tryPersistMessages(conversationId, userId, userContent, result) {
+  if (!userId || !conversationId) return;
+  try {
+    const owns = await verifyConversationOwner(conversationId, userId);
+    if (!owns) return;
+    await Promise.all([
+      addMessage(conversationId, { role: "user", content: userContent }),
+      addMessage(conversationId, { role: "assistant", content: result.answer, sources: result.sources })
+    ]);
+  } catch (err) {
+    console.error("[chat] falha ao salvar mensagem:", err);
+  }
+}
+
 export const ChatController = {
   Responder: async function (req, res, next) {
     try {
@@ -12,20 +26,7 @@ export const ChatController = {
         options: body.options
       });
 
-      if (req.userId && body.conversationId) {
-        try {
-          const owns = await verifyConversationOwner(body.conversationId, req.userId);
-          if (owns) {
-            await Promise.all([
-              addMessage(body.conversationId, { role: "user", content: body.message }),
-              addMessage(body.conversationId, { role: "assistant", content: result.answer, sources: result.sources })
-            ]);
-          }
-        } catch (err) {
-          console.error("[chat] falha ao salvar mensagem:", err);
-        }
-      }
-
+      await tryPersistMessages(body.conversationId, req.userId, body.message, result);
       res.json(result);
     } catch (err) {
       next(err);
@@ -67,17 +68,9 @@ export const ChatController = {
       res.write("data: [DONE]\n\n");
       res.end();
 
-      if (req.userId && body.conversationId) {
-        verifyConversationOwner(body.conversationId, req.userId)
-          .then((owns) => {
-            if (!owns) return;
-            return Promise.all([
-              addMessage(body.conversationId, { role: "user", content: body.message }),
-              addMessage(body.conversationId, { role: "assistant", content: result.answer, sources: result.sources })
-            ]);
-          })
-          .catch((err) => console.error("[chat] falha ao salvar mensagem:", err));
-      }
+      tryPersistMessages(body.conversationId, req.userId, body.message, result).catch(
+        (err) => console.error("[chat] falha ao salvar mensagem:", err)
+      );
     } catch (err) {
       if (!res.headersSent) {
         next(err);

+ 3 - 2
src/controllers/Conversations.Controller.js

@@ -1,4 +1,5 @@
 import { z } from "zod";
+import { NotFoundError } from "../shared/errors/index.js";
 import {
   listConversations,
   createConversation,
@@ -33,7 +34,7 @@ export const ConversationsController = {
   Mensagens: async function (req, res, next) {
     try {
       const msgs = await getConversationMessages(req.params.id, req.userId);
-      if (!msgs) return res.status(404).json({ error: "not_found" });
+      if (!msgs) throw new NotFoundError("Conversa não encontrada");
       res.json({ items: msgs });
     } catch (err) {
       next(err);
@@ -62,7 +63,7 @@ export const ConversationsController = {
   Exportar: async function (req, res, next) {
     try {
       const markdown = await exportConversationAsMarkdown(req.params.id, req.userId);
-      if (markdown == null) return res.status(404).json({ error: "not_found" });
+      if (markdown == null) throw new NotFoundError("Conversa não encontrada");
       res.json({ markdown });
     } catch (err) {
       next(err);

+ 4 - 11
src/controllers/Ingest.Controller.js

@@ -1,3 +1,4 @@
+import { BadRequestError } from "../shared/errors/index.js";
 import { extractDocumentsFromUpload, ingestDocuments, fetchUrlText } from "../services/ingestService.js";
 
 export const IngestController = {
@@ -13,10 +14,7 @@ export const IngestController = {
   IngerirArquivo: async function (req, res, next) {
     try {
       const f = req.file;
-      if (!f?.buffer) {
-        res.status(400).json({ error: "file_required" });
-        return;
-      }
+      if (!f?.buffer) throw new BadRequestError("file_required");
 
       const source = typeof req.body?.source === "string" && req.body.source.trim() ? req.body.source.trim() : undefined;
       const docs = await extractDocumentsFromUpload({
@@ -27,10 +25,7 @@ export const IngestController = {
       });
 
       const extractedChars = docs.reduce((acc, d) => acc + (d?.text?.length ?? 0), 0);
-      if (!extractedChars) {
-        res.status(400).json({ error: "empty_extracted_text" });
-        return;
-      }
+      if (!extractedChars) throw new BadRequestError("empty_extracted_text");
 
       const result = await ingestDocuments(docs);
       res.json({ ...result, documents: docs.length, extractedChars });
@@ -43,9 +38,7 @@ export const IngestController = {
     try {
       const body = req.body;
       const text = await fetchUrlText(body.url);
-      if (!text) {
-        return res.status(400).json({ error: "empty_extracted_text" });
-      }
+      if (!text) throw new BadRequestError("empty_extracted_text");
       const source = body.source ?? body.url;
       const result = await ingestDocuments([{ text, source, title: source }]);
       res.json({ ...result, source, extractedChars: text.length });

+ 10 - 13
src/controllers/Usuario.Controller.js

@@ -1,4 +1,5 @@
 import bcrypt from "bcryptjs";
+import { AppError, NotFoundError, BadRequestError } from "../shared/errors/index.js";
 import { Usuario } from "../models/Usuario.model.js";
 import { formatarUsuario } from "../utils/formatarUsuario.js";
 
@@ -24,9 +25,7 @@ export const UsuarioController = {
 
       const loginTrimado = Login.trim();
       const existente = await Usuario.query().findOne({ Login: loginTrimado });
-      if (existente) {
-        return res.status(409).send({ status: false, msg: "Login já cadastrado!" });
-      }
+      if (existente) throw new AppError("Login já cadastrado!", 409);
 
       const senhaHash = await bcrypt.hash(Senha, 10);
       const usuario = await Usuario.query().insert({
@@ -48,7 +47,7 @@ export const UsuarioController = {
   Atualizar: async function (req, res, next) {
     try {
       const id = Number(req.params.id);
-      if (!id) return res.status(400).send({ status: false, msg: "ID inválido!" });
+      if (!id) throw new BadRequestError("ID inválido");
 
       const { Nome, Email, Nivel, Setor } = req.body;
       const updates = {};
@@ -58,7 +57,7 @@ export const UsuarioController = {
       if (Setor !== undefined) updates.Setor = Setor.trim();
 
       const usuario = await Usuario.query().patchAndFetchById(id, updates);
-      if (!usuario) return res.status(404).send({ status: false, msg: "Usuário não encontrado!" });
+      if (!usuario) throw new NotFoundError("Usuário não encontrado");
 
       return res.status(200).send({ status: true, usuario: formatarUsuario(usuario) });
     } catch (error) {
@@ -69,10 +68,10 @@ export const UsuarioController = {
   ToggleStatus: async function (req, res, next) {
     try {
       const id = Number(req.params.id);
-      if (!id) return res.status(400).send({ status: false, msg: "ID inválido!" });
+      if (!id) throw new BadRequestError("ID inválido");
 
       const usuario = await Usuario.query().findById(id);
-      if (!usuario) return res.status(404).send({ status: false, msg: "Usuário não encontrado!" });
+      if (!usuario) throw new NotFoundError("Usuário não encontrado");
 
       const novoStatus = String(usuario.Status) === "1" ? "0" : "1";
       await Usuario.query().findById(id).patch({ Status: novoStatus });
@@ -86,23 +85,21 @@ export const UsuarioController = {
   AlterarSenha: async function (req, res, next) {
     try {
       const id = Number(req.params.id);
-      if (!id) return res.status(400).send({ status: false, msg: "ID inválido!" });
+      if (!id) throw new BadRequestError("ID inválido");
 
       const callerIsOwner = String(req.user?.sub) === String(id);
       const callerIsAdmin = String(req.user?.nivel) === "3";
       if (!callerIsOwner && !callerIsAdmin) {
-        return res.status(403).send({ status: false, msg: "Sem permissão para alterar a senha deste usuário." });
+        throw new AppError("Sem permissão para alterar a senha deste usuário", 403);
       }
 
       const { senhaAtual, senhaNova } = req.body;
 
       const usuario = await Usuario.query().findById(id);
-      if (!usuario) return res.status(404).send({ status: false, msg: "Usuário não encontrado!" });
+      if (!usuario) throw new NotFoundError("Usuário não encontrado");
 
       const senhaValida = bcrypt.compareSync(senhaAtual, String(usuario.Senha ?? ""));
-      if (!senhaValida) {
-        return res.status(401).send({ status: false, msg: "Senha atual incorreta!" });
-      }
+      if (!senhaValida) throw new AppError("Senha atual incorreta", 401);
 
       const novaHash = bcrypt.hashSync(senhaNova, 10);
       await Usuario.query().findById(id).patch({ Senha: novaHash });

+ 3 - 1
src/middleware/parseId.js

@@ -1,7 +1,9 @@
+import { BadRequestError } from "../shared/errors/index.js";
+
 export function parseId(req, res, next) {
   const id = Number(req.params.id);
   if (!Number.isInteger(id) || id <= 0) {
-    return res.status(400).json({ error: "invalid_id" });
+    return next(new BadRequestError("invalid_id"));
   }
   req.params.id = id;
   next();

+ 3 - 1
src/middleware/requireUser.js

@@ -1,6 +1,8 @@
+import { UnauthorizedError } from "../shared/errors/index.js";
+
 export function requireUser(req, res, next) {
   const userId = req.user?.sub;
-  if (!userId) return res.status(401).json({ error: "unauthorized" });
+  if (!userId) return next(new UnauthorizedError());
   req.userId = userId;
   next();
 }

+ 11 - 0
src/middleware/schemas/Auth.Schema.js

@@ -0,0 +1,11 @@
+import { z } from "zod";
+
+export const loginSchema = z.object({
+  login: z.string().min(1).max(100),
+  senha: z.string().min(1).max(128),
+  rememberMe: z.boolean().optional()
+});
+
+export const refreshSchema = z.object({
+  refreshToken: z.string().min(1)
+});

+ 4 - 2
src/routes/Auth.Rotas.js

@@ -1,8 +1,10 @@
 import { Router } from "express";
 import { AuthController } from "../controllers/Auth.Controller.js";
+import { validate } from "../middleware/Validate.js";
+import { loginSchema, refreshSchema } from "../middleware/schemas/Auth.Schema.js";
 
 export const authRouter = Router();
 
-authRouter.post("/login", AuthController.Login);
+authRouter.post("/login", validate({ body: loginSchema }), AuthController.Login);
 authRouter.post("/logout", AuthController.Logout);
-authRouter.post("/refresh", AuthController.Refresh);
+authRouter.post("/refresh", validate({ body: refreshSchema }), AuthController.Refresh);