GabrielRamison há 3 meses atrás
pai
commit
5a33f013fc

+ 0 - 7
.env

@@ -1,7 +0,0 @@
-PORT=3001
-CORS_ORIGIN=http://localhost:5173
-QDRANT_URL=http://localhost:6333
-QDRANT_COLLECTION=empresa_docs
-OLLAMA_URL=http://localhost:11434
-OLLAMA_EMBEDDINGS_MODEL=nomic-embed-text
-OLLAMA_CHAT_MODEL=llama3.1

+ 6 - 2
.gitignore

@@ -1,3 +1,7 @@
-node_modules/
+package-lock.json
+/node_modules/*
+yarn.lock
+.yarn
+/data/*
+.env
 .DS_Store
-yarn.lock

+ 5 - 5
app.js

@@ -2,11 +2,11 @@ import express from "express";
 import cors from "cors";
 import helmet from "helmet";
 import morgan from "morgan";
-import { config } from "./config/index.js";
-import { authMiddleware } from "./middleware/auth.js";
-import { rateLimitMiddleware } from "./middleware/rateLimit.js";
-import { errorHandler } from "./middleware/errorHandler.js";
-import { apiRouter } from "./routes/index.js";
+import { config } from "./src/config/index.js";
+import { authMiddleware } from "./src/middleware/auth.js";
+import { rateLimitMiddleware } from "./src/middleware/rateLimit.js";
+import { errorHandler } from "./src/middleware/errorHandler.js";
+import { apiRouter } from "./src/routes/index.js";
 
 export function createApp() {
   const app = express();

+ 3 - 3
chat/chatChain.js

@@ -1,6 +1,6 @@
-import { config } from "../config/index.js";
-import { chatCompletion } from "../services/ollamaClient.js";
-import { searchDocs } from "./searchChat.js";
+import { config } from "../src/config/index.js";
+import { chatCompletion } from "../src/services/ollamaClient.js";
+import { searchDocs } from "../chat/searchChat.js";
 import fs from "node:fs";
 
 function buildContextBlock(hits) {

+ 5 - 5
chat/ingest.js

@@ -1,8 +1,8 @@
-import { config } from "../config/index.js";
-import { chunkText } from "../services/textChunker.js";
-import { embedTexts, visionExtractFromImage } from "../services/ollamaClient.js";
-import { ensureCollection } from "../services/collectionService.js";
-import { qdrant } from "../services/qdrantClient.js";
+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 { createHash } from "node:crypto";
 import { createRequire } from "node:module";
 import fs from "node:fs";

+ 3 - 3
chat/searchChat.js

@@ -1,6 +1,6 @@
-import { config } from "../config/index.js";
-import { embedTexts } from "../services/ollamaClient.js";
-import { qdrant } from "../services/qdrantClient.js";
+import { config } from "../src/config/index.js";
+import { embedTexts } from "../src/services/ollamaClient.js";
+import { qdrant } from "../src/services/qdrantClient.js";
 import fs from "node:fs";
 
 function isRefusalText(text) {

+ 19 - 0
db/migrations/20260609164817_create_usuarios_table.cjs

@@ -0,0 +1,19 @@
+
+exports.up = function(knex, Promise) {
+    return knex.schema.createTable('usuarios', t => {
+        t.increments('Id')
+        t.string('Nome').notNullable()
+        t.string('Login').notNullable()
+        t.string('Senha').notNullable()
+        t.string('Email').notNullable()
+        t.string('Status').notNullable()
+        t.string('Nivel').notNullable()
+        t.string('Setor').notNullable()
+    })
+
+  
+};
+
+exports.down = function(knex, Promise) {
+  return knex.schema.dropTable('usuarios')
+};

+ 19 - 0
db/migrations/20260609170200_create_refresh_tokens_table.cjs

@@ -0,0 +1,19 @@
+exports.up = function (knex) {
+  return knex.schema.createTable("refresh_tokens", (t) => {
+    t.increments("Id");
+    t.integer("UsuarioId").unsigned().notNullable();
+    t.string("TokenHash", 64).notNullable().unique();
+    t.dateTime("ExpiresAt").notNullable();
+    t.dateTime("RevokedAt").nullable();
+    t.timestamp("CreatedAt").notNullable().defaultTo(knex.fn.now());
+
+    t.foreign("UsuarioId").references("usuarios.Id").onDelete("CASCADE");
+    t.index(["UsuarioId"]);
+    t.index(["ExpiresAt"]);
+  });
+};
+
+exports.down = function (knex) {
+  return knex.schema.dropTable("refresh_tokens");
+};
+

+ 16 - 0
db/seeds/usuario.js

@@ -0,0 +1,16 @@
+export async function seed(knex) {
+  const usuarios = [
+    {
+      Id: 1,
+      Nome: "Leonardo",
+      Login: "leonardo",
+      Email: "leonardo@star.psi.br",
+      Senha: "$2b$08$OckwNAnmdnIyjiDtCRpXN./1h2pphmTFdKpQz.U3ZLHt63Rq7NmHC",
+      Status: 1,
+      Nivel: 1,
+      Setor: 1
+    }
+  ];
+
+  await knex("usuarios").insert(usuarios).onConflict("Id").ignore();
+}

+ 24 - 0
knexfile.js

@@ -0,0 +1,24 @@
+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",
+      collation: "utf8mb4_bin"
+    },
+    migrations: {
+      tableName: "knex_migrations",
+      directory: "./db/migrations"
+    },
+    seeds: {
+      tableName: "knex_seeds",
+      directory: "./db/seeds"
+    }
+  }
+};

+ 0 - 15
middleware/auth.js

@@ -1,15 +0,0 @@
-import { config } from "../config/index.js";
-
-export function authMiddleware(req, res, next) {
-  if (!config.apiKey) return next();
-
-  const header = req.header("authorization") ?? "";
-  const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
-
-  if (token !== config.apiKey) {
-    res.status(401).json({ error: "unauthorized" });
-    return;
-  }
-
-  next();
-}

+ 6 - 0
package.json

@@ -5,6 +5,9 @@
   "scripts": {
     "dev": "node --watch server.js",
     "start": "node server.js",
+    "migrate:latest": "knex migrate:latest",
+    "migrate:rollback": "knex migrate:rollback",
+    "seed:run": "knex seed:run",
     "lint": "node -c server.js && node -c app.js",
     "build": "node -c server.js"
   },
@@ -14,9 +17,12 @@
     "dotenv": "^16.4.5",
     "express": "^4.19.2",
     "helmet": "^7.1.0",
+    "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"
   },

+ 1 - 1
server.js

@@ -1,5 +1,5 @@
 import { createApp } from "./app.js";
-import { config } from "./config/index.js";
+import { config } from "./src/config/index.js";
 
 const app = createApp();
 

+ 0 - 0
config/collections.js → src/config/collections.js


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

@@ -0,0 +1,10 @@
+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
+};

+ 9 - 0
config/index.js → src/config/index.js

@@ -6,6 +6,15 @@ export const config = {
   port: Number(process.env.PORT ?? 3001),
   corsOrigin: process.env.CORS_ORIGIN ?? "http://localhost:5173",
   apiKey: process.env.API_KEY ?? "",
+  auth: {
+    mode: process.env.AUTH_MODE ?? "none"
+  },
+  jwt: {
+    secret: process.env.JWT_SECRET ?? "",
+    issuer: process.env.JWT_ISSUER ?? "oraculo-api",
+    accessTtlSeconds: Number(process.env.JWT_ACCESS_TTL_SECONDS ?? 900),
+    refreshTtlSeconds: Number(process.env.JWT_REFRESH_TTL_SECONDS ?? 60 * 60 * 24 * 30)
+  },
   qdrant: {
     url: process.env.QDRANT_URL ?? "http://localhost:6333",
     apiKey: process.env.QDRANT_API_KEY ?? "",

+ 7 - 0
src/config/server.js

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

+ 76 - 0
src/controllers/Usuario.Controller.js

@@ -0,0 +1,76 @@
+import bcrypt from "bcryptjs";
+import { db } from "../db/knex.js";
+
+async function recordFailedAttempt(_login) {}
+
+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 = {
+    Login: async function (req, res, next) {
+        try {
+            const { Login, login, Senha, senha, RemenberMe = false } = req.body ?? {};
+            const userAgent = req.headers["user-agent"] || "unknown";
+            void RemenberMe;
+            void userAgent;
+
+            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 usuario = await db("usuarios")
+                .where(loginInformado.includes("@") ? { Email: loginInformado } : { Login: loginInformado })
+                .first();
+
+            if (!usuario) {
+                await recordFailedAttempt(loginInformado);
+                return res.status(401).send({ status: false, msg: "Usuario não localizado!" });
+            }
+
+            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) {
+                await recordFailedAttempt(loginInformado);
+                return res.status(401).send({ status: false, msg: "Combinacao de usuario e senho invalida!" });
+            }
+
+            return res.status(200).send({
+                status: true,
+                msg: "Login realizado com sucesso!",
+                usuario: formatarUsuario(usuario)
+            });
+        } catch (error) {
+            return next(error);
+        }
+    },
+
+    Logout: async function (_req, res, next) {
+        try {
+            return res.status(200).send({ status: true, msg: "Logout realizado com sucesso!" });
+        } catch (error) {
+            return next(error);
+        }
+    }
+
+     
+};
+
+export default UsuarioController;

+ 16 - 0
src/db/knex.js

@@ -0,0 +1,16 @@
+import "dotenv/config";
+import knex from "knex";
+
+export const db = knex({
+  client: "mysql2",
+  connection: {
+    host: process.env.DB_HOST,
+    user: process.env.DB_USER,
+    port: process.env.DB_PORT ? Number(process.env.DB_PORT) : undefined,
+    password: process.env.DB_PASS,
+    database: process.env.DB_SCHEMA,
+    charset: "utf8mb4",
+    collation: "utf8mb4_bin"
+  }
+});
+

+ 46 - 0
src/middleware/auth.js

@@ -0,0 +1,46 @@
+import { config } from "../config/index.js";
+import { verifyAccessToken } from "../services/authTokens.js";
+
+export function authMiddleware(req, res, next) {
+  if (config.auth.mode === "none") {
+    next();
+    return;
+  }
+
+  const header = req.header("authorization") ?? "";
+  const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length).trim() : "";
+
+  const isPublic = req.path === "/auth/login" || req.path === "/auth/logout";
+
+  const hasAuthConfigured = Boolean(config.apiKey || config.jwt.secret);
+  if (!hasAuthConfigured) return next();
+
+  if (token && config.apiKey && token === config.apiKey) {
+    req.user = { sub: "system", system: true };
+    next();
+    return;
+  }
+
+  if (token && config.jwt.secret) {
+    try {
+      const payload = verifyAccessToken(token);
+      if (payload) {
+        req.user = payload;
+        next();
+        return;
+      }
+    } catch (err) {
+      if (!isPublic) {
+        res.status(401).json({ error: "unauthorized" });
+        return;
+      }
+    }
+  }
+
+  if (isPublic) {
+    next();
+    return;
+  }
+
+  res.status(401).json({ error: "unauthorized" });
+}

+ 0 - 0
middleware/errorHandler.js → src/middleware/errorHandler.js


+ 0 - 0
middleware/rateLimit.js → src/middleware/rateLimit.js


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

@@ -0,0 +1,35 @@
+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;

+ 7 - 0
src/routes/auth.js

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

+ 1 - 1
routes/chat.js → src/routes/chat.js

@@ -1,6 +1,6 @@
 import { Router } from "express";
 import { z } from "zod";
-import { answerWithContext } from "../chat/chatChain.js";
+import { answerWithContext } from "../../chat/chatChain.js";
 import fs from "node:fs";
 
 export const chatRouter = Router();

+ 0 - 0
routes/documents.js → src/routes/documents.js


+ 2 - 0
routes/index.js → src/routes/index.js

@@ -3,9 +3,11 @@ import { chatRouter } from "./chat.js";
 import { searchRouter } from "./search.js";
 import { ingestRouter } from "./ingest.js";
 import { documentsRouter } from "./documents.js";
+import { authRouter } from "./auth.js";
 
 export const apiRouter = Router();
 
+apiRouter.use("/auth", authRouter);
 apiRouter.use("/chat", chatRouter);
 apiRouter.use("/search", searchRouter);
 apiRouter.use("/ingest", ingestRouter);

+ 1 - 1
routes/ingest.js → src/routes/ingest.js

@@ -1,7 +1,7 @@
 import { Router } from "express";
 import { z } from "zod";
 import multer from "multer";
-import { extractDocumentsFromUpload, ingestDocuments } from "../chat/ingest.js";
+import { extractDocumentsFromUpload, ingestDocuments } from "../../chat/ingest.js";
 import fs from "node:fs";
 
 export const ingestRouter = Router();

+ 1 - 1
routes/search.js → 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 "../../chat/searchChat.js";
 
 export const searchRouter = Router();
 

+ 32 - 0
src/services/authTokens.js

@@ -0,0 +1,32 @@
+import crypto from "node:crypto";
+import jwt from "jsonwebtoken";
+import { config } from "../config/index.js";
+
+export function hashRefreshToken(token) {
+  return crypto.createHash("sha256").update(token).digest("hex");
+}
+
+export function generateRefreshToken() {
+  return crypto.randomBytes(32).toString("base64url");
+}
+
+export function signAccessToken(payload) {
+  if (!config.jwt.secret) {
+    const err = new Error("jwt_not_configured");
+    err.statusCode = 500;
+    throw err;
+  }
+
+  return jwt.sign(payload, config.jwt.secret, {
+    issuer: config.jwt.issuer,
+    expiresIn: config.jwt.accessTtlSeconds
+  });
+}
+
+export function verifyAccessToken(token) {
+  if (!config.jwt.secret) return null;
+  return jwt.verify(token, config.jwt.secret, {
+    issuer: config.jwt.issuer
+  });
+}
+

+ 0 - 0
services/collectionService.js → src/services/collectionService.js


+ 0 - 0
services/documentsService.js → src/services/documentsService.js


+ 0 - 0
services/loader.js → src/services/loader.js


+ 0 - 0
services/ollama.js → src/services/ollama.js


+ 0 - 0
services/ollamaClient.js → src/services/ollamaClient.js


+ 0 - 0
services/qdrant.js → src/services/qdrant.js


+ 0 - 0
services/qdrantClient.js → src/services/qdrantClient.js


+ 0 - 0
services/textChunker.js → src/services/textChunker.js