leonardo 2 miesięcy temu
rodzic
commit
962b3e269d

+ 39 - 34
src/controllers/Usuario.Controller.js

@@ -1,8 +1,32 @@
 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";
 
+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;
@@ -178,31 +202,23 @@ export const UsuarioController = {
     Criar: async function (req, res, next) {
         if (!requireAdmin(req, res)) return;
         try {
-            const { Nome, Login, Email, Senha, Nivel, Setor } = req.body ?? {};
-
-            if (!Nome || !Login || !Email || !Senha || !Nivel || !Setor) {
-                return res.status(400).send({ status: false, msg: "Todos os campos são obrigatórios!" });
-            }
-
-            if (String(Senha).length < 6) {
-                return res.status(400).send({ status: false, msg: "Senha deve ter pelo menos 6 caracteres!" });
-            }
+            const { Nome, Login, Email, Senha, Nivel, Setor } = criarSchema.parse(req.body ?? {});
 
-            const loginTrimado = String(Login).trim();
+            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(String(Senha), 10);
+            const senhaHash = await bcrypt.hash(Senha, 10);
             const [id] = await db("usuarios").insert({
-                Nome: String(Nome).trim(),
+                Nome: Nome.trim(),
                 Login: loginTrimado,
-                Email: String(Email).trim(),
+                Email: Email.trim(),
                 Senha: senhaHash,
                 Status: "1",
-                Nivel: String(Nivel),
-                Setor: String(Setor).trim()
+                Nivel,
+                Setor: Setor.trim()
             });
 
             const usuario = await db("usuarios").where({ Id: id }).first();
@@ -218,16 +234,12 @@ export const UsuarioController = {
             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 { Nome, Email, Nivel, Setor } = atualizarSchema.parse(req.body ?? {});
             const updates = {};
-            if (Nome) updates.Nome = String(Nome).trim();
-            if (Email) updates.Email = String(Email).trim();
-            if (Nivel !== undefined) updates.Nivel = String(Nivel);
-            if (Setor) updates.Setor = String(Setor).trim();
-
-            if (Object.keys(updates).length === 0) {
-                return res.status(400).send({ status: false, msg: "Nenhum campo para atualizar!" });
-            }
+            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();
@@ -262,24 +274,17 @@ export const UsuarioController = {
             const id = Number(req.params.id);
             if (!id) return res.status(400).send({ status: false, msg: "ID inválido!" });
 
-            const { senhaAtual, senhaNova } = req.body ?? {};
-            if (!senhaAtual || !senhaNova) {
-                return res.status(400).send({ status: false, msg: "Senhas obrigatórias!" });
-            }
+            const { senhaAtual, senhaNova } = senhaSchema.parse(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 senhaValida = bcrypt.compareSync(String(senhaAtual), String(usuario.Senha ?? ""));
+            const senhaValida = bcrypt.compareSync(senhaAtual, String(usuario.Senha ?? ""));
             if (!senhaValida) {
                 return res.status(401).send({ status: false, msg: "Senha atual incorreta!" });
             }
 
-            if (String(senhaNova).length < 6) {
-                return res.status(400).send({ status: false, msg: "Nova senha deve ter pelo menos 6 caracteres!" });
-            }
-
-            const novaHash = bcrypt.hashSync(String(senhaNova), 10);
+            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!" });

+ 5 - 6
src/routes/chat.js

@@ -2,6 +2,7 @@ import { Router } from "express";
 import { z } from "zod";
 import { answerWithContext, answerWithContextStream } from "../../chat/chatChain.js";
 import { addMessage } from "../services/conversationsService.js";
+import { requireUser } from "../middleware/requireUser.js";
 
 export const chatRouter = Router();
 
@@ -18,7 +19,7 @@ const chatBodySchema = z.object({
     .optional()
 });
 
-chatRouter.post("/", async (req, res, next) => {
+chatRouter.post("/", requireUser, async (req, res, next) => {
   try {
     const body = chatBodySchema.parse(req.body);
     const result = await answerWithContext({
@@ -27,8 +28,7 @@ chatRouter.post("/", async (req, res, next) => {
       options: body.options
     });
 
-    const userId = req.user?.sub;
-    if (userId && body.conversationId) {
+    if (req.userId && body.conversationId) {
       try {
         await Promise.all([
           addMessage(body.conversationId, { role: "user", content: body.message }),
@@ -45,7 +45,7 @@ chatRouter.post("/", async (req, res, next) => {
   }
 });
 
-chatRouter.post("/stream", async (req, res, next) => {
+chatRouter.post("/stream", requireUser, async (req, res, next) => {
   let abortController;
   try {
     const body = chatBodySchema.parse(req.body);
@@ -73,8 +73,7 @@ chatRouter.post("/stream", async (req, res, next) => {
     res.write("data: [DONE]\n\n");
     res.end();
 
-    const userId = req.user?.sub;
-    if (userId && body.conversationId) {
+    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 })

+ 29 - 2
src/routes/conversations.js

@@ -5,7 +5,8 @@ import {
   createConversation,
   getConversationMessages,
   updateConversationTitle,
-  deleteConversation
+  deleteConversation,
+  getConversationForExport
 } from "../services/conversationsService.js";
 import { requireUser } from "../middleware/requireUser.js";
 
@@ -13,7 +14,9 @@ export const conversationsRouter = Router();
 
 conversationsRouter.get("/", requireUser, async (req, res, next) => {
   try {
-    const items = await listConversations(req.userId);
+    const limit  = z.coerce.number().int().positive().max(100).catch(50).parse(req.query.limit);
+    const offset = z.coerce.number().int().nonnegative().catch(0).parse(req.query.offset);
+    const items  = await listConversations(req.userId, { limit, offset });
     res.json({ items });
   } catch (err) {
     next(err);
@@ -64,3 +67,27 @@ conversationsRouter.delete("/:id", requireUser, async (req, res, next) => {
     next(err);
   }
 });
+
+conversationsRouter.get("/:id/export", requireUser, 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") });
+  } catch (err) {
+    next(err);
+  }
+});

+ 1 - 4
src/routes/documents.js

@@ -18,10 +18,7 @@ documentsRouter.get("/", requireUser, async (req, res, next) => {
 
 documentsRouter.delete("/source/:source", requireUser, async (req, res, next) => {
   try {
-    const source = req.params.source;
-    if (!source) {
-      return res.status(400).json({ error: "source_required" });
-    }
+    const source = z.string().min(1).max(255).parse(decodeURIComponent(req.params.source));
     await deleteDocumentsBySource(source);
     res.json({ ok: true });
   } catch (err) {

+ 17 - 1
src/services/conversationsService.js

@@ -1,9 +1,13 @@
 import { db } from "../db/knex.js";
 
-export async function listConversations(userId) {
+export async function listConversations(userId, { limit = 50, offset = 0 } = {}) {
+  const safeLimit = Math.min(Number(limit) || 50, 100);
+  const safeOffset = Math.max(Number(offset) || 0, 0);
   return db("conversations")
     .where({ UsuarioId: userId })
     .orderBy("UpdatedAt", "desc")
+    .limit(safeLimit)
+    .offset(safeOffset)
     .select("Id", "Title", "CreatedAt", "UpdatedAt");
 }
 
@@ -59,3 +63,15 @@ export async function updateConversationTitle(conversationId, userId, title) {
 export async function deleteConversation(conversationId, userId) {
   await db("conversations").where({ Id: conversationId, UsuarioId: userId }).delete();
 }
+
+export async function getConversationForExport(conversationId, userId) {
+  const conv = await db("conversations").where({ Id: conversationId, UsuarioId: userId }).first();
+  if (!conv) return null;
+
+  const msgs = await db("messages")
+    .where({ ConversationId: conversationId })
+    .orderBy("SentAt", "asc")
+    .select("Role", "Content", "SentAt");
+
+  return { title: conv.Title, createdAt: conv.CreatedAt, messages: msgs };
+}