浏览代码

ajustes backend

leonardo 2 月之前
父节点
当前提交
2141afd574

+ 10 - 0
.env.example

@@ -37,3 +37,13 @@ RAG_CHUNK_OVERLAP=150
 
 # Rate limiting (0 = desativado)
 RATE_LIMIT_PER_MINUTE=0
+
+# Ajuste fino das respostas do LLM
+LLM_TEMPERATURE=0.7
+LLM_TOP_P=0.9
+LLM_NUM_PREDICT=2048
+LLM_REPEAT_PENALTY=1.1
+
+# RAG avançado
+RAG_QUERY_REWRITE=false
+RAG_MIN_SCORE=0.45

+ 22 - 2
app.js

@@ -3,6 +3,8 @@ import cors from "cors";
 import helmet from "helmet";
 import morgan from "morgan";
 import { config } from "./src/config/index.js";
+import { db } from "./src/db/knex.js";
+import { qdrant } from "./src/services/qdrantClient.js";
 import { authMiddleware } from "./src/middleware/auth.js";
 import { rateLimitMiddleware } from "./src/middleware/rateLimit.js";
 import { errorHandler } from "./src/middleware/errorHandler.js";
@@ -22,8 +24,26 @@ export function createApp() {
   app.use(express.json({ limit: "10mb" }));
   app.use(morgan("dev"));
 
-  app.get("/health", (_req, res) => {
-    res.json({ ok: true });
+  app.get("/health", async (_req, res) => {
+    const checks = { api: true, db: false, qdrant: false, ollama: false };
+
+    try {
+      await db.raw("SELECT 1");
+      checks.db = true;
+    } catch {}
+
+    try {
+      await qdrant.getCollections();
+      checks.qdrant = true;
+    } catch {}
+
+    try {
+      const r = await fetch(`${config.ollama.url}/api/tags`, { signal: AbortSignal.timeout(3000) });
+      checks.ollama = r.ok;
+    } catch {}
+
+    const ok = Object.values(checks).every(Boolean);
+    res.status(ok ? 200 : 503).json({ ok, checks });
   });
 
   app.use("/api", rateLimitMiddleware, authMiddleware, apiRouter);

+ 3 - 3
db/seeds/usuario.js

@@ -6,9 +6,9 @@ export async function seed(knex) {
       Login: "leonardo",
       Email: "leonardo@star.psi.br",
       Senha: "$2b$08$OckwNAnmdnIyjiDtCRpXN./1h2pphmTFdKpQz.U3ZLHt63Rq7NmHC",
-      Status: 1,
-      Nivel: 1,
-      Setor: 1
+      Status: "1",
+      Nivel: "1",
+      Setor: "TI"
     }
   ];
 

+ 22 - 19
knexfile.js

@@ -1,23 +1,26 @@
 import "dotenv/config";
 
-export default {
-  development: {
-    client: "mysql",
-    connection: {
-      host: process.env.DB_HOST,
-      user: process.env.DB_USER,
-      port: process.env.DB_PORT,
-      password: process.env.DB_PASS,
-      database: process.env.DB_SCHEMA,
-      charset: "utf8mb4",
-    },
-    migrations: {
-      tableName: "knex_migrations",
-      directory: "./db/migrations"
-    },
-    seeds: {
-      tableName: "knex_seeds",
-      directory: "./db/seeds"
-    }
+const base = {
+  client: "mysql2",
+  connection: {
+    host:     process.env.DB_HOST,
+    user:     process.env.DB_USER,
+    port:     process.env.DB_PORT,
+    password: process.env.DB_PASS,
+    database: process.env.DB_SCHEMA,
+    charset:  "utf8mb4"
+  },
+  migrations: {
+    tableName: "knex_migrations",
+    directory: "./db/migrations"
+  },
+  seeds: {
+    tableName: "knex_seeds",
+    directory: "./db/seeds"
   }
 };
+
+export default {
+  development: base,
+  production:  { ...base, connection: { ...base.connection, ssl: { rejectUnauthorized: false } } }
+};

+ 0 - 5
loaders/cloudDrive.js

@@ -1,5 +0,0 @@
-export async function syncCloudDrive() {
-  const err = new Error("cloud_drive_sync_not_configured");
-  err.statusCode = 501;
-  throw err;
-}

+ 0 - 5
loaders/driveLoader.js

@@ -1,5 +0,0 @@
-export async function syncGoogleDrive() {
-  const err = new Error("google_drive_sync_not_configured");
-  err.statusCode = 501;
-  throw err;
-}

+ 0 - 5
loaders/pdfLoader.js

@@ -1,5 +0,0 @@
-export async function loadPdf() {
-  const err = new Error("pdf_loader_not_configured");
-  err.statusCode = 501;
-  throw err;
-}

+ 0 - 12
loaders/txtLoader.js

@@ -1,12 +0,0 @@
-export async function loadTxtFromString(text, { source, metadata } = {}) {
-  const normalized = String(text ?? "").trim();
-  if (!normalized) return [];
-
-  return [
-    {
-      text: normalized,
-      source: source ?? "txt",
-      metadata: metadata ?? null
-    }
-  ];
-}

+ 2 - 1
package.json

@@ -13,15 +13,16 @@
   },
   "dependencies": {
     "@qdrant/js-client-rest": "^1.11.0",
+    "bcryptjs": "^2.4.3",
     "cors": "^2.8.5",
     "dotenv": "^16.4.5",
     "express": "^4.19.2",
     "helmet": "^7.1.0",
+    "jsonwebtoken": "^9.0.2",
     "knex": "^3.2.10",
     "mammoth": "^1.8.0",
     "morgan": "^1.10.0",
     "multer": "^2.0.0",
-    "mysql": "^2.18.1",
     "mysql2": "^3.22.5",
     "pdf-parse": "^1.1.1",
     "zod": "^3.23.8"

+ 2 - 0
server.js

@@ -1,8 +1,10 @@
 import { createApp } from "./app.js";
 import { config } from "./src/config/index.js";
+import { scheduleCleanup } from "./src/jobs/cleanupTokens.js";
 
 const app = createApp();
 
 app.listen(config.port, () => {
   process.stdout.write(`API listening on http://localhost:${config.port}\n`);
+  scheduleCleanup();
 });

+ 10 - 10
chat/chatChain.js → src/chat/chatChain.js

@@ -1,7 +1,7 @@
-import { config } from "../src/config/index.js";
-import { chatCompletion, chatCompletionStream } from "../src/services/ollamaClient.js";
-import { getRecentMessages } from "../src/services/conversationsService.js";
-import { searchDocs } from "../chat/searchChat.js";
+import { config } from "../config/index.js";
+import { chatCompletion, chatCompletionStream } from "../services/ollamaClient.js";
+import { getRecentMessages } from "../services/conversationsService.js";
+import { searchDocs } from "../services/searchService.js";
 
 const SYSTEM_PROMPT = [
   "Você é o Oráculo, assistente interno da empresa Star.",
@@ -77,10 +77,10 @@ async function rewriteQuery(query) {
   }
 }
 
-async function loadHistory(conversationId) {
+async function loadHistory(conversationId, userId) {
   if (!conversationId) return [];
   try {
-    return await getRecentMessages(conversationId, 12);
+    return await getRecentMessages(conversationId, userId, 12);
   } catch {
     return [];
   }
@@ -98,11 +98,11 @@ function buildDefaultOptions(override) {
     : defaults;
 }
 
-export async function answerWithContext({ message, conversationId, options }) {
+export async function answerWithContext({ message, conversationId, userId, options }) {
   const searchQuery = config.rag.queryRewrite ? await rewriteQuery(message) : message;
   const hits = await searchDocs({ query: searchQuery, topK: config.rag.topK });
   const context = buildContextBlock(hits);
-  const history = await loadHistory(conversationId);
+  const history = await loadHistory(conversationId, userId);
   const messages = buildMessages(message, context, history);
   const completion = await chatCompletion({ messages, options: buildDefaultOptions(options) });
 
@@ -112,11 +112,11 @@ export async function answerWithContext({ message, conversationId, options }) {
   };
 }
 
-export async function answerWithContextStream({ message, conversationId, options, onChunk, signal }) {
+export async function answerWithContextStream({ message, conversationId, userId, options, onChunk, signal }) {
   const searchQuery = config.rag.queryRewrite ? await rewriteQuery(message) : message;
   const hits = await searchDocs({ query: searchQuery, topK: config.rag.topK });
   const context = buildContextBlock(hits);
-  const history = await loadHistory(conversationId);
+  const history = await loadHistory(conversationId, userId);
   const messages = buildMessages(message, context, history);
   const sources = hitsToSources(hits);
 

+ 0 - 5
src/config/collections.js

@@ -1,5 +0,0 @@
-import { config } from "./index.js";
-
-export const collections = {
-  docs: config.qdrant.collection
-};

+ 0 - 10
src/config/db.config.js

@@ -1,10 +0,0 @@
-import { db } from "../db/knex.js";
-
-// Compat layer for legacy imports that still expect a db.config module.
-export const knex = db;
-export const connection = null;
-
-export default {
-  knex,
-  connection
-};

+ 4 - 1
src/config/index.js

@@ -18,7 +18,7 @@ export const config = {
   qdrant: {
     url: process.env.QDRANT_URL ?? "http://localhost:6333",
     apiKey: process.env.QDRANT_API_KEY ?? "",
-    collection: process.env.QDRANT_COLLECTION ?? "empresa_docs"
+    collection: process.env.QDRANT_COLLECTION ?? "oraculo_docs"
   },
   ollama: {
     url: process.env.OLLAMA_URL ?? "http://localhost:11434",
@@ -38,5 +38,8 @@ export const config = {
     topP: Number(process.env.LLM_TOP_P ?? 0.9),
     numPredict: Number(process.env.LLM_NUM_PREDICT ?? 2048),
     repeatPenalty: Number(process.env.LLM_REPEAT_PENALTY ?? 1.1)
+  },
+  rateLimit: {
+    perMinute: Number(process.env.RATE_LIMIT_PER_MINUTE ?? 0)
   }
 };

+ 0 - 7
src/config/server.js

@@ -1,7 +0,0 @@
-import "dotenv/config";
-
-export const port = process.env.SERVER_PORT ?? 4000;
-
-export default {
-  port
-};

+ 144 - 0
src/controllers/Auth.Controller.js

@@ -0,0 +1,144 @@
+import bcrypt from "bcryptjs";
+import { db } from "../db/knex.js";
+import { signAccessToken, generateRefreshToken, hashRefreshToken } from "../services/authTokens.js";
+import { config } from "../config/index.js";
+import { formatarUsuario } from "../utils/formatarUsuario.js";
+
+const _failedAttempts = new Map();
+const MAX_ATTEMPTS = 5;
+const BLOCK_MS = 15 * 60 * 1000;
+
+function recordFailedAttempt(login) {
+  const now = Date.now();
+  const entry = _failedAttempts.get(login) ?? { count: 0, blockedUntil: 0 };
+  entry.count += 1;
+  if (entry.count >= MAX_ATTEMPTS) {
+    entry.blockedUntil = now + BLOCK_MS;
+    entry.count = 0;
+  }
+  _failedAttempts.set(login, entry);
+}
+
+function isLoginBlocked(login) {
+  const entry = _failedAttempts.get(login);
+  if (!entry) return false;
+  if (entry.blockedUntil > Date.now()) return true;
+  if (entry.blockedUntil > 0) _failedAttempts.delete(login);
+  return false;
+}
+
+export const AuthController = {
+  Login: async function (req, res, next) {
+    try {
+      const { Login, login, Senha, 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);
+
+      if (isLoginBlocked(loginInformado)) {
+        return res.status(429).send({ status: false, msg: "Muitas tentativas incorretas. Tente novamente em 15 minutos." });
+      }
+
+      const usuario = await db("usuarios")
+        .where(loginInformado.includes("@") ? { Email: loginInformado } : { Login: loginInformado })
+        .first();
+
+      if (!usuario) {
+        recordFailedAttempt(loginInformado);
+        return res.status(401).send({ status: false, msg: "Combinação de usuário e senha inválida!" });
+      }
+
+      if (String(usuario.Status) === "0") {
+        return res.status(401).send({ status: false, msg: "Usuario inativo!" });
+      }
+
+      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!" });
+      }
+
+      _failedAttempts.delete(loginInformado);
+
+      let accessToken = null;
+      let refreshToken = null;
+
+      if (config.jwt.secret) {
+        accessToken = signAccessToken({ sub: usuario.Id, login: usuario.Login, nivel: usuario.Nivel });
+        refreshToken = generateRefreshToken();
+        const tokenHash = hashRefreshToken(refreshToken);
+        const expiresAt = new Date(Date.now() + config.jwt.refreshTtlSeconds * 1000);
+        await db("refresh_tokens").insert({
+          UsuarioId: usuario.Id,
+          TokenHash: tokenHash,
+          ExpiresAt: expiresAt
+        });
+      }
+
+      return res.status(200).send({
+        status: true,
+        msg: "Login realizado com sucesso!",
+        usuario: formatarUsuario(usuario),
+        ...(accessToken ? { accessToken, refreshToken } : {})
+      });
+    } catch (error) {
+      return next(error);
+    }
+  },
+
+  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 tokenHash = hashRefreshToken(refreshToken);
+      const stored = await db("refresh_tokens")
+        .where({ TokenHash: tokenHash })
+        .whereNull("RevokedAt")
+        .where("ExpiresAt", ">", new Date())
+        .first();
+
+      if (!stored) {
+        return res.status(401).send({ status: false, msg: "Refresh token inválido ou expirado!" });
+      }
+
+      const usuario = await db("usuarios").where({ Id: stored.UsuarioId }).first();
+      if (!usuario || String(usuario.Status) === "0") {
+        return res.status(401).send({ status: false, msg: "Usuario inativo!" });
+      }
+
+      const accessToken = signAccessToken({ sub: usuario.Id, login: usuario.Login, nivel: usuario.Nivel });
+
+      return res.status(200).send({ status: true, accessToken });
+    } catch (error) {
+      return next(error);
+    }
+  },
+
+  Logout: async function (req, res, next) {
+    try {
+      const { refreshToken } = req.body ?? {};
+      if (refreshToken) {
+        const tokenHash = hashRefreshToken(refreshToken);
+        await db("refresh_tokens")
+          .where({ TokenHash: tokenHash })
+          .whereNull("RevokedAt")
+          .update({ RevokedAt: new Date() });
+      }
+      return res.status(200).send({ status: true, msg: "Logout realizado com sucesso!" });
+    } catch (error) {
+      return next(error);
+    }
+  }
+};
+
+export default AuthController;

+ 99 - 277
src/controllers/Usuario.Controller.js

@@ -1,297 +1,119 @@
 import bcrypt from "bcryptjs";
-import { z } from "zod";
 import { db } from "../db/knex.js";
-import { signAccessToken, generateRefreshToken, hashRefreshToken } from "../services/authTokens.js";
-import { config } from "../config/index.js";
+import { formatarUsuario } from "../utils/formatarUsuario.js";
 
-const criarSchema = z.object({
-  Nome:  z.string().min(1).max(100),
-  Login: z.string().min(3).max(50),
-  Email: z.string().email().max(150),
-  Senha: z.string().min(6).max(128),
-  Nivel: z.enum(["1", "2", "3"]),
-  Setor: z.string().min(1).max(100)
-});
-
-const atualizarSchema = z.object({
-  Nome:  z.string().min(1).max(100).optional(),
-  Email: z.string().email().max(150).optional(),
-  Nivel: z.enum(["1", "2", "3"]).optional(),
-  Setor: z.string().min(1).max(100).optional()
-}).refine(obj => Object.values(obj).some(v => v !== undefined), {
-  message: "Nenhum campo para atualizar"
-});
-
-const senhaSchema = z.object({
-  senhaAtual: z.string().min(1),
-  senhaNova:  z.string().min(6).max(128)
-});
-
-const _failedAttempts = new Map();
-const MAX_ATTEMPTS = 5;
-const BLOCK_MS = 15 * 60 * 1000;
-
-function recordFailedAttempt(login) {
-    const now = Date.now();
-    const entry = _failedAttempts.get(login) ?? { count: 0, blockedUntil: 0 };
-    entry.count += 1;
-    if (entry.count >= MAX_ATTEMPTS) {
-        entry.blockedUntil = now + BLOCK_MS;
-        entry.count = 0;
+export const UsuarioController = {
+  Listar: async function (req, res, next) {
+    try {
+      const usuarios = await db("usuarios")
+        .select("Id", "Nome", "Login", "Email", "Status", "Nivel", "Setor")
+        .orderBy("Nome", "asc");
+
+      return res.status(200).send({
+        status: true,
+        items: usuarios.map(formatarUsuario)
+      });
+    } catch (error) {
+      return next(error);
     }
-    _failedAttempts.set(login, entry);
-}
-
-function isLoginBlocked(login) {
-    const entry = _failedAttempts.get(login);
-    if (!entry) return false;
-    if (entry.blockedUntil > Date.now()) return true;
-    if (entry.blockedUntil > 0) _failedAttempts.delete(login);
-    return false;
-}
-
-function requireAdmin(req, res) {
-    if (String(req.user?.nivel) !== "3") {
-        res.status(403).send({ status: false, msg: "Acesso negado. Apenas administradores podem realizar esta ação." });
-        return false;
+  },
+
+  Criar: async function (req, res, next) {
+    try {
+      const { Nome, Login, Email, Senha, Nivel, Setor } = req.body;
+
+      const loginTrimado = Login.trim();
+      const existente = await db("usuarios").where({ Login: loginTrimado }).first();
+      if (existente) {
+        return res.status(409).send({ status: false, msg: "Login já cadastrado!" });
+      }
+
+      const senhaHash = await bcrypt.hash(Senha, 10);
+      const [id] = await db("usuarios").insert({
+        Nome: Nome.trim(),
+        Login: loginTrimado,
+        Email: Email.trim(),
+        Senha: senhaHash,
+        Status: "1",
+        Nivel,
+        Setor: Setor.trim()
+      });
+
+      const usuario = await db("usuarios").where({ Id: id }).first();
+      return res.status(201).send({ status: true, usuario: formatarUsuario(usuario) });
+    } catch (error) {
+      return next(error);
     }
-    return true;
-}
-
-function formatarUsuario(usuario) {
-    return {
-        Id: usuario.Id,
-        Nome: usuario.Nome,
-        Login: usuario.Login,
-        Email: usuario.Email,
-        Status: usuario.Status,
-        Nivel: usuario.Nivel,
-        Setor: usuario.Setor
-    };
-}
-
-export const UsuarioController = {
-    Listar: async function (req, res, next) {
-        if (!requireAdmin(req, res)) return;
-        try {
-            const usuarios = await db("usuarios")
-                .select("Id", "Nome", "Login", "Email", "Status", "Nivel", "Setor")
-                .orderBy("Nome", "asc");
-
-            return res.status(200).send({
-                status: true,
-                items: usuarios.map(formatarUsuario)
-            });
-        } catch (error) {
-            return next(error);
-        }
-    },
-
-    Login: async function (req, res, next) {
-        try {
-            const { Login, login, Senha, 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);
-
-            if (isLoginBlocked(loginInformado)) {
-                return res.status(429).send({ status: false, msg: "Muitas tentativas incorretas. Tente novamente em 15 minutos." });
-            }
-
-            const usuario = await db("usuarios")
-                .where(loginInformado.includes("@") ? { Email: loginInformado } : { Login: loginInformado })
-                .first();
-
-            if (!usuario) {
-                recordFailedAttempt(loginInformado);
-                return res.status(401).send({ status: false, msg: "Combinação de usuário e senha inválida!" });
-            }
-
-            if (String(usuario.Status) === "0") {
-                return res.status(401).send({ status: false, msg: "Usuario inativo!" });
-            }
-
-            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!" });
-            }
-
-            _failedAttempts.delete(loginInformado);
-
-            let accessToken = null;
-            let refreshToken = null;
-
-            if (config.jwt.secret) {
-                accessToken = signAccessToken({ sub: usuario.Id, login: usuario.Login, nivel: usuario.Nivel });
-                refreshToken = generateRefreshToken();
-                const tokenHash = hashRefreshToken(refreshToken);
-                const expiresAt = new Date(Date.now() + config.jwt.refreshTtlSeconds * 1000);
-                await db("refresh_tokens").insert({
-                    UsuarioId: usuario.Id,
-                    TokenHash: tokenHash,
-                    ExpiresAt: expiresAt
-                });
-            }
-
-            return res.status(200).send({
-                status: true,
-                msg: "Login realizado com sucesso!",
-                usuario: formatarUsuario(usuario),
-                ...(accessToken ? { accessToken, refreshToken } : {})
-            });
-        } catch (error) {
-            return next(error);
-        }
-    },
-
-    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 tokenHash = hashRefreshToken(refreshToken);
-            const stored = await db("refresh_tokens")
-                .where({ TokenHash: tokenHash })
-                .whereNull("RevokedAt")
-                .where("ExpiresAt", ">", new Date())
-                .first();
-
-            if (!stored) {
-                return res.status(401).send({ status: false, msg: "Refresh token inválido ou expirado!" });
-            }
-
-            const usuario = await db("usuarios").where({ Id: stored.UsuarioId }).first();
-            if (!usuario || String(usuario.Status) === "0") {
-                return res.status(401).send({ status: false, msg: "Usuario inativo!" });
-            }
-
-            const accessToken = signAccessToken({ sub: usuario.Id, login: usuario.Login, nivel: usuario.Nivel });
-
-            return res.status(200).send({ status: true, accessToken });
-        } catch (error) {
-            return next(error);
-        }
-    },
-
-    Logout: async function (req, res, next) {
-        try {
-            const { refreshToken } = req.body ?? {};
-            if (refreshToken) {
-                const tokenHash = hashRefreshToken(refreshToken);
-                await db("refresh_tokens")
-                    .where({ TokenHash: tokenHash })
-                    .whereNull("RevokedAt")
-                    .update({ RevokedAt: new Date() });
-            }
-            return res.status(200).send({ status: true, msg: "Logout realizado com sucesso!" });
-        } catch (error) {
-            return next(error);
-        }
-    },
-
-    Criar: async function (req, res, next) {
-        if (!requireAdmin(req, res)) return;
-        try {
-            const { Nome, Login, Email, Senha, Nivel, Setor } = criarSchema.parse(req.body ?? {});
-
-            const loginTrimado = Login.trim();
-            const existente = await db("usuarios").where({ Login: loginTrimado }).first();
-            if (existente) {
-                return res.status(409).send({ status: false, msg: "Login já cadastrado!" });
-            }
-
-            const senhaHash = await bcrypt.hash(Senha, 10);
-            const [id] = await db("usuarios").insert({
-                Nome: Nome.trim(),
-                Login: loginTrimado,
-                Email: Email.trim(),
-                Senha: senhaHash,
-                Status: "1",
-                Nivel,
-                Setor: Setor.trim()
-            });
-
-            const usuario = await db("usuarios").where({ Id: id }).first();
-            return res.status(201).send({ status: true, usuario: formatarUsuario(usuario) });
-        } catch (error) {
-            return next(error);
-        }
-    },
-
-    Atualizar: async function (req, res, next) {
-        if (!requireAdmin(req, res)) return;
-        try {
-            const id = Number(req.params.id);
-            if (!id) return res.status(400).send({ status: false, msg: "ID inválido!" });
-
-            const { Nome, Email, Nivel, Setor } = atualizarSchema.parse(req.body ?? {});
-            const updates = {};
-            if (Nome  !== undefined) updates.Nome  = Nome.trim();
-            if (Email !== undefined) updates.Email = Email.trim();
-            if (Nivel !== undefined) updates.Nivel = Nivel;
-            if (Setor !== undefined) updates.Setor = Setor.trim();
-
-            await db("usuarios").where({ Id: id }).update(updates);
-            const usuario = await db("usuarios").where({ Id: id }).first();
-            if (!usuario) return res.status(404).send({ status: false, msg: "Usuário não encontrado!" });
+  },
+
+  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!" });
+
+      const { Nome, Email, Nivel, Setor } = req.body;
+      const updates = {};
+      if (Nome  !== undefined) updates.Nome  = Nome.trim();
+      if (Email !== undefined) updates.Email = Email.trim();
+      if (Nivel !== undefined) updates.Nivel = Nivel;
+      if (Setor !== undefined) updates.Setor = Setor.trim();
+
+      await db("usuarios").where({ Id: id }).update(updates);
+      const usuario = await db("usuarios").where({ Id: id }).first();
+      if (!usuario) return res.status(404).send({ status: false, msg: "Usuário não encontrado!" });
+
+      return res.status(200).send({ status: true, usuario: formatarUsuario(usuario) });
+    } catch (error) {
+      return next(error);
+    }
+  },
 
-            return res.status(200).send({ status: true, usuario: formatarUsuario(usuario) });
-        } catch (error) {
-            return next(error);
-        }
-    },
+  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!" });
 
-    ToggleStatus: async function (req, res, next) {
-        if (!requireAdmin(req, res)) return;
-        try {
-            const id = Number(req.params.id);
-            if (!id) return res.status(400).send({ status: false, msg: "ID inválido!" });
+      const usuario = await db("usuarios").where({ Id: id }).first();
+      if (!usuario) return res.status(404).send({ status: false, msg: "Usuário não encontrado!" });
 
-            const usuario = await db("usuarios").where({ Id: id }).first();
-            if (!usuario) return res.status(404).send({ status: false, msg: "Usuário não encontrado!" });
+      const novoStatus = String(usuario.Status) === "1" ? "0" : "1";
+      await db("usuarios").where({ Id: id }).update({ Status: novoStatus });
 
-            const novoStatus = String(usuario.Status) === "1" ? "0" : "1";
-            await db("usuarios").where({ Id: id }).update({ Status: novoStatus });
+      return res.status(200).send({ status: true, novoStatus });
+    } catch (error) {
+      return next(error);
+    }
+  },
 
-            return res.status(200).send({ status: true, novoStatus });
-        } catch (error) {
-            return next(error);
-        }
-    },
+  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!" });
 
-    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!" });
+      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." });
+      }
 
-            const { senhaAtual, senhaNova } = senhaSchema.parse(req.body ?? {});
+      const { senhaAtual, senhaNova } = req.body;
 
-            const usuario = await db("usuarios").where({ Id: id }).first();
-            if (!usuario) return res.status(404).send({ status: false, msg: "Usuário não encontrado!" });
+      const usuario = await db("usuarios").where({ Id: id }).first();
+      if (!usuario) return res.status(404).send({ status: false, msg: "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!" });
-            }
+      const senhaValida = bcrypt.compareSync(senhaAtual, String(usuario.Senha ?? ""));
+      if (!senhaValida) {
+        return res.status(401).send({ status: false, msg: "Senha atual incorreta!" });
+      }
 
-            const novaHash = bcrypt.hashSync(senhaNova, 10);
-            await db("usuarios").where({ Id: id }).update({ Senha: novaHash });
+      const novaHash = bcrypt.hashSync(senhaNova, 10);
+      await db("usuarios").where({ Id: id }).update({ Senha: novaHash });
 
-            return res.status(200).send({ status: true, msg: "Senha alterada com sucesso!" });
-        } catch (error) {
-            return next(error);
-        }
+      return res.status(200).send({ status: true, msg: "Senha alterada com sucesso!" });
+    } catch (error) {
+      return next(error);
     }
+  }
 };
 
 export default UsuarioController;

+ 14 - 0
src/jobs/cleanupTokens.js

@@ -0,0 +1,14 @@
+import { db } from "../db/knex.js";
+
+export async function cleanupExpiredTokens() {
+  await db("refresh_tokens")
+    .where("ExpiresAt", "<", new Date())
+    .delete();
+}
+
+export function scheduleCleanup(intervalMs = 6 * 60 * 60 * 1000) {
+  cleanupExpiredTokens().catch(() => {});
+  return setInterval(() => {
+    cleanupExpiredTokens().catch(() => {});
+  }, intervalMs).unref();
+}

+ 8 - 0
src/middleware/parseId.js

@@ -0,0 +1,8 @@
+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" });
+  }
+  req.params.id = id;
+  next();
+}

+ 3 - 1
src/middleware/rateLimit.js

@@ -1,3 +1,5 @@
+import { config } from "../config/index.js";
+
 const buckets = new Map();
 
 setInterval(() => {
@@ -8,7 +10,7 @@ setInterval(() => {
 }, 60_000).unref();
 
 export function rateLimitMiddleware(req, res, next) {
-  const limitPerMinute = Number(process.env.RATE_LIMIT_PER_MINUTE ?? 0);
+  const limitPerMinute = config.rateLimit.perMinute;
   if (!limitPerMinute) return next();
 
   const key = req.ip ?? "unknown";

+ 9 - 0
src/middleware/requireAdmin.js

@@ -0,0 +1,9 @@
+export function requireAdmin(req, res, next) {
+  if (String(req.user?.nivel) !== "3") {
+    return res.status(403).json({
+      status: false,
+      msg: "Acesso negado. Apenas administradores podem realizar esta ação."
+    });
+  }
+  next();
+}

+ 24 - 0
src/middleware/schemas/usuario.js

@@ -0,0 +1,24 @@
+import { z } from "zod";
+
+export const criarSchema = z.object({
+  Nome:  z.string().min(1).max(100),
+  Login: z.string().min(3).max(50),
+  Email: z.string().email().max(150),
+  Senha: z.string().min(6).max(128),
+  Nivel: z.enum(["1", "2", "3"]),
+  Setor: z.string().min(1).max(100)
+});
+
+export const atualizarSchema = z.object({
+  Nome:  z.string().min(1).max(100).optional(),
+  Email: z.string().email().max(150).optional(),
+  Nivel: z.enum(["1", "2", "3"]).optional(),
+  Setor: z.string().min(1).max(100).optional()
+}).refine(obj => Object.values(obj).some(v => v !== undefined), {
+  message: "Nenhum campo para atualizar"
+});
+
+export const senhaSchema = z.object({
+  senhaAtual: z.string().min(1),
+  senhaNova:  z.string().min(6).max(128)
+});

+ 10 - 0
src/middleware/validate.js

@@ -0,0 +1,10 @@
+export function validate(schema) {
+  return (req, _res, next) => {
+    try {
+      req.body = schema.parse(req.body ?? {});
+      next();
+    } catch (err) {
+      next(err);
+    }
+  };
+}

+ 0 - 35
src/models/Usuario.model.js

@@ -1,35 +0,0 @@
-import { knex } from "../config/db.config.js";
-
-function formatUsuario(usuario) {
-  if (!usuario) return usuario;
-
-  const formatted = { ...usuario };
-
-  try {
-    formatted.Perfil = JSON.parse(formatted.Perfil);
-  } catch (_error) {
-    formatted.Perfil = [];
-  }
-
-  return formatted;
-}
-
-export class Usuario {
-  static get tableName() {
-    return "usuarios";
-  }
-
-  static get idColumn() {
-    return "Id";
-  }
-
-  static query() {
-    const queryBuilder = knex(this.tableName);
-    const originalFirst = queryBuilder.first.bind(queryBuilder);
-
-    queryBuilder.first = async (...args) => formatUsuario(await originalFirst(...args));
-    return queryBuilder;
-  }
-}
-
-export default Usuario;

+ 4 - 4
src/routes/auth.js

@@ -1,8 +1,8 @@
 import { Router } from "express";
-import { UsuarioController } from "../controllers/Usuario.Controller.js";
+import { AuthController } from "../controllers/Auth.Controller.js";
 
 export const authRouter = Router();
 
-authRouter.post("/login", UsuarioController.Login);
-authRouter.post("/logout", UsuarioController.Logout);
-authRouter.post("/refresh", UsuarioController.Refresh);
+authRouter.post("/login", AuthController.Login);
+authRouter.post("/logout", AuthController.Logout);
+authRouter.post("/refresh", AuthController.Refresh);

+ 20 - 10
src/routes/chat.js

@@ -1,7 +1,7 @@
 import { Router } from "express";
 import { z } from "zod";
-import { answerWithContext, answerWithContextStream } from "../../chat/chatChain.js";
-import { addMessage } from "../services/conversationsService.js";
+import { answerWithContext, answerWithContextStream } from "../chat/chatChain.js";
+import { addMessage, verifyConversationOwner } from "../services/conversationsService.js";
 import { requireUser } from "../middleware/requireUser.js";
 
 export const chatRouter = Router();
@@ -25,15 +25,19 @@ chatRouter.post("/", requireUser, async (req, res, next) => {
     const result = await answerWithContext({
       message: body.message,
       conversationId: body.conversationId,
+      userId: req.userId,
       options: body.options
     });
 
     if (req.userId && body.conversationId) {
       try {
-        await Promise.all([
-          addMessage(body.conversationId, { role: "user", content: body.message }),
-          addMessage(body.conversationId, { role: "assistant", content: result.answer, sources: result.sources })
-        ]);
+        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);
       }
@@ -62,6 +66,7 @@ chatRouter.post("/stream", requireUser, async (req, res, next) => {
     const result = await answerWithContextStream({
       message: body.message,
       conversationId: body.conversationId,
+      userId: req.userId,
       options: body.options,
       signal: abortController.signal,
       onChunk: (delta) => {
@@ -74,10 +79,15 @@ chatRouter.post("/stream", requireUser, async (req, res, next) => {
     res.end();
 
     if (req.userId && body.conversationId) {
-      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));
+      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));
     }
   } catch (err) {
     if (!res.headersSent) {

+ 12 - 31
src/routes/conversations.js

@@ -6,9 +6,10 @@ import {
   getConversationMessages,
   updateConversationTitle,
   deleteConversation,
-  getConversationForExport
+  exportConversationAsMarkdown
 } from "../services/conversationsService.js";
 import { requireUser } from "../middleware/requireUser.js";
+import { parseId } from "../middleware/parseId.js";
 
 export const conversationsRouter = Router();
 
@@ -33,11 +34,9 @@ conversationsRouter.post("/", requireUser, async (req, res, next) => {
   }
 });
 
-conversationsRouter.get("/:id/messages", requireUser, async (req, res, next) => {
+conversationsRouter.get("/:id/messages", requireUser, parseId, async (req, res, next) => {
   try {
-    const conversationId = Number(req.params.id);
-    if (!conversationId) return res.status(400).json({ error: "invalid_id" });
-    const msgs = await getConversationMessages(conversationId, req.userId);
+    const msgs = await getConversationMessages(req.params.id, req.userId);
     if (!msgs) return res.status(404).json({ error: "not_found" });
     res.json({ items: msgs });
   } catch (err) {
@@ -45,48 +44,30 @@ conversationsRouter.get("/:id/messages", requireUser, async (req, res, next) =>
   }
 });
 
-conversationsRouter.patch("/:id", requireUser, async (req, res, next) => {
+conversationsRouter.patch("/:id", requireUser, parseId, async (req, res, next) => {
   try {
-    const conversationId = Number(req.params.id);
-    if (!conversationId) return res.status(400).json({ error: "invalid_id" });
     const title = z.string().min(1).max(200).parse(req.body?.title);
-    await updateConversationTitle(conversationId, req.userId, title);
+    await updateConversationTitle(req.params.id, req.userId, title);
     res.json({ ok: true });
   } catch (err) {
     next(err);
   }
 });
 
-conversationsRouter.delete("/:id", requireUser, async (req, res, next) => {
+conversationsRouter.delete("/:id", requireUser, parseId, async (req, res, next) => {
   try {
-    const conversationId = Number(req.params.id);
-    if (!conversationId) return res.status(400).json({ error: "invalid_id" });
-    await deleteConversation(conversationId, req.userId);
+    await deleteConversation(req.params.id, req.userId);
     res.json({ ok: true });
   } catch (err) {
     next(err);
   }
 });
 
-conversationsRouter.get("/:id/export", requireUser, async (req, res, next) => {
+conversationsRouter.get("/:id/export", requireUser, parseId, async (req, res, next) => {
   try {
-    const conversationId = Number(req.params.id);
-    if (!conversationId) return res.status(400).json({ error: "invalid_id" });
-    const data = await getConversationForExport(conversationId, req.userId);
-    if (!data) return res.status(404).json({ error: "not_found" });
-
-    const date = new Date(data.createdAt).toLocaleDateString("pt-BR", {
-      day: "2-digit", month: "2-digit", year: "numeric"
-    });
-
-    const lines = [`# ${data.title}`, `*Exportado em ${date}*`, ""];
-    for (const msg of data.messages) {
-      const author = msg.Role === "user" ? "**Você**" : "**Oráculo**";
-      lines.push(`${author}:`, "", msg.Content, "");
-      lines.push("---", "");
-    }
-
-    res.json({ markdown: lines.join("\n") });
+    const markdown = await exportConversationAsMarkdown(req.params.id, req.userId);
+    if (markdown == null) return res.status(404).json({ error: "not_found" });
+    res.json({ markdown });
   } catch (err) {
     next(err);
   }

+ 1 - 1
src/routes/ingest.js

@@ -1,7 +1,7 @@
 import { Router } from "express";
 import { z } from "zod";
 import multer from "multer";
-import { extractDocumentsFromUpload, ingestDocuments, fetchUrlText } from "../../chat/ingest.js";
+import { extractDocumentsFromUpload, ingestDocuments, fetchUrlText } from "../services/ingestService.js";
 import { requireUser } from "../middleware/requireUser.js";
 
 export const ingestRouter = Router();

+ 1 - 1
src/routes/search.js

@@ -1,6 +1,6 @@
 import { Router } from "express";
 import { z } from "zod";
-import { searchDocs } from "../../chat/searchChat.js";
+import { searchDocs } from "../services/searchService.js";
 import { requireUser } from "../middleware/requireUser.js";
 
 export const searchRouter = Router();

+ 9 - 5
src/routes/users.js

@@ -1,10 +1,14 @@
 import { Router } from "express";
 import { UsuarioController } from "../controllers/Usuario.Controller.js";
+import { requireUser } from "../middleware/requireUser.js";
+import { requireAdmin } from "../middleware/requireAdmin.js";
+import { validate } from "../middleware/validate.js";
+import { criarSchema, atualizarSchema, senhaSchema } from "../middleware/schemas/usuario.js";
 
 export const usersRouter = Router();
 
-usersRouter.get("/", UsuarioController.Listar);
-usersRouter.post("/", UsuarioController.Criar);
-usersRouter.put("/:id", UsuarioController.Atualizar);
-usersRouter.patch("/:id/status", UsuarioController.ToggleStatus);
-usersRouter.patch("/:id/senha", UsuarioController.AlterarSenha);
+usersRouter.get("/",             requireUser, requireAdmin,                        UsuarioController.Listar);
+usersRouter.post("/",            requireUser, requireAdmin, validate(criarSchema), UsuarioController.Criar);
+usersRouter.put("/:id",          requireUser, requireAdmin, validate(atualizarSchema), UsuarioController.Atualizar);
+usersRouter.patch("/:id/status", requireUser, requireAdmin,                        UsuarioController.ToggleStatus);
+usersRouter.patch("/:id/senha",  requireUser,               validate(senhaSchema), UsuarioController.AlterarSenha);

+ 28 - 1
src/services/conversationsService.js

@@ -31,7 +31,16 @@ export async function getConversationMessages(conversationId, userId) {
   return msgs;
 }
 
-export async function getRecentMessages(conversationId, limit = 12) {
+export async function verifyConversationOwner(conversationId, userId) {
+  const conv = await db("conversations").where({ Id: conversationId, UsuarioId: userId }).first();
+  return Boolean(conv);
+}
+
+export async function getRecentMessages(conversationId, userId, limit = 12) {
+  if (userId) {
+    const owns = await verifyConversationOwner(conversationId, userId);
+    if (!owns) return [];
+  }
   const msgs = await db("messages")
     .where({ ConversationId: conversationId })
     .orderBy("SentAt", "desc")
@@ -75,3 +84,21 @@ export async function getConversationForExport(conversationId, userId) {
 
   return { title: conv.Title, createdAt: conv.CreatedAt, messages: msgs };
 }
+
+export async function exportConversationAsMarkdown(conversationId, userId) {
+  const data = await getConversationForExport(conversationId, userId);
+  if (!data) return null;
+
+  const date = new Date(data.createdAt).toLocaleDateString("pt-BR", {
+    day: "2-digit", month: "2-digit", year: "numeric"
+  });
+
+  const lines = [`# ${data.title}`, `*Exportado em ${date}*`, ""];
+  for (const msg of data.messages) {
+    const author = msg.Role === "user" ? "**Você**" : "**Oráculo**";
+    lines.push(`${author}:`, "", msg.Content, "");
+    lines.push("---", "");
+  }
+
+  return lines.join("\n");
+}

+ 2 - 1
src/services/documentsService.js

@@ -24,7 +24,8 @@ export async function listDocuments({ limit, offset }) {
     title: p.payload?.title ?? null,
     text: p.payload?.text ?? null,
     chunkIndex: p.payload?.chunkIndex ?? null,
-    metadata: p.payload?.metadata ?? null
+    metadata: p.payload?.metadata ?? null,
+    ingestedAt: p.payload?.ingestedAt ?? null
   }));
 
   const nextOffset = points?.next_page_offset ?? points?.nextPageOffset ?? null;

+ 10 - 17
chat/ingest.js → src/services/ingestService.js

@@ -1,22 +1,15 @@
-import { config } from "../src/config/index.js";
-import { chunkText } from "../src/services/textChunker.js";
-import { embedTexts, visionExtractFromImage } from "../src/services/ollamaClient.js";
-import { ensureCollection } from "../src/services/collectionService.js";
-import { qdrant } from "../src/services/qdrantClient.js";
+import { config } from "../config/index.js";
+import { chunkText } from "./textChunker.js";
+import { embedTexts, visionExtractFromImage } from "./ollamaClient.js";
+import { ensureCollection } from "./collectionService.js";
+import { qdrant } from "./qdrantClient.js";
+import { isRefusal } from "../utils/isRefusal.js";
 import { createHash } from "node:crypto";
 import { createRequire } from "node:module";
 import mammoth from "mammoth";
 
 const require = createRequire(import.meta.url);
 
-function isVisionRefusal(text) {
-  const t = String(text ?? "").toLowerCase();
-  return (
-    t.includes("desculpe") &&
-    (t.includes("não posso") || t.includes("nao posso") || t.includes("não posso fornecer") || t.includes("nao posso fornecer"))
-  );
-}
-
 function visionPrompt() {
   return [
     "Analise a imagem (print de tela / manual interno).",
@@ -248,7 +241,7 @@ export async function extractDocumentsFromUpload({ buffer, filename, mimeType, s
       try {
         let r = await visionExtractFromImage({ imageBase64, prompt: visionPrompt() });
         let t = String(r?.content ?? "").trim();
-        if (isVisionRefusal(t)) {
+        if (isRefusal(t)) {
           r = await visionExtractFromImage({
             imageBase64,
             prompt: [
@@ -260,7 +253,7 @@ export async function extractDocumentsFromUpload({ buffer, filename, mimeType, s
           });
           t = String(r?.content ?? "").trim();
         }
-        if (isVisionRefusal(t)) {
+        if (isRefusal(t)) {
           visionError = "vision_refused";
           visionSkipped += 1;
           continue;
@@ -303,7 +296,7 @@ export async function extractDocumentsFromUpload({ buffer, filename, mimeType, s
     try {
       let r = await visionExtractFromImage({ imageBase64, prompt: visionPrompt() });
       let text = String(r?.content ?? "").trim();
-      if (isVisionRefusal(text)) {
+      if (isRefusal(text)) {
         r = await visionExtractFromImage({
           imageBase64,
           prompt: [
@@ -315,7 +308,7 @@ export async function extractDocumentsFromUpload({ buffer, filename, mimeType, s
         });
         text = String(r?.content ?? "").trim();
       }
-      if (isVisionRefusal(text)) {
+      if (isRefusal(text)) {
         const err = new Error("vision_refused");
         err.statusCode = 400;
         throw err;

+ 0 - 1
src/services/loader.js

@@ -1 +0,0 @@
-export { chunkText } from "./textChunker.js";

+ 0 - 1
src/services/ollama.js

@@ -1 +0,0 @@
-export { embedTexts, chatCompletion } from "./ollamaClient.js";

+ 0 - 1
src/services/qdrant.js

@@ -1 +0,0 @@
-export { qdrant } from "./qdrantClient.js";

+ 5 - 12
chat/searchChat.js → src/services/searchService.js

@@ -1,14 +1,7 @@
-import { config } from "../src/config/index.js";
-import { embedTexts } from "../src/services/ollamaClient.js";
-import { qdrant } from "../src/services/qdrantClient.js";
-
-function isRefusalText(text) {
-  const t = String(text ?? "").toLowerCase();
-  return (
-    t.includes("desculpe") &&
-    (t.includes("não posso") || t.includes("nao posso") || t.includes("não posso fornecer") || t.includes("nao posso fornecer"))
-  );
-}
+import { config } from "../config/index.js";
+import { embedTexts } from "./ollamaClient.js";
+import { qdrant } from "./qdrantClient.js";
+import { isRefusal } from "../utils/isRefusal.js";
 
 export async function searchDocs({ query, topK }) {
   const collectionName = config.qdrant.collection;
@@ -31,5 +24,5 @@ export async function searchDocs({ query, topK }) {
       chunkIndex: r.payload?.chunkIndex ?? null,
       metadata: r.payload?.metadata ?? null
     }))
-    .filter((h) => !isRefusalText(h.text));
+    .filter((h) => !isRefusal(h.text));
 }

+ 11 - 0
src/utils/formatarUsuario.js

@@ -0,0 +1,11 @@
+export function formatarUsuario(usuario) {
+  return {
+    Id: usuario.Id,
+    Nome: usuario.Nome,
+    Login: usuario.Login,
+    Email: usuario.Email,
+    Status: usuario.Status,
+    Nivel: usuario.Nivel,
+    Setor: usuario.Setor
+  };
+}

+ 7 - 0
src/utils/isRefusal.js

@@ -0,0 +1,7 @@
+export function isRefusal(text) {
+  const t = String(text ?? "").toLowerCase();
+  return (
+    t.includes("desculpe") &&
+    (t.includes("não posso") || t.includes("nao posso") || t.includes("não posso fornecer") || t.includes("nao posso fornecer"))
+  );
+}