leonardo 1 周之前
父节点
当前提交
75ac42118a

+ 40 - 8
src/controllers/Atendimentos.Controller.js

@@ -23,6 +23,38 @@ import { estruturarMensagem, montarUrlMidia } from "../utils/atendimentoFormat.j
 import { parseFiltrosAvaliacoes } from "../utils/atendimentoAvaliacaoFiltros.js";
 import { config } from "../config/index.js";
 
+
+function inteiroOuUndefined(valor) {
+  if (typeof valor !== "string") return undefined;
+  const texto = valor.trim();
+  if (!texto || !/^-?\d+$/.test(texto)) return undefined;
+  const numero = Number(texto);
+  return Number.isSafeInteger(numero) ? numero : undefined;
+}
+
+
+function inteiroPositivoOu(valor, padrao) {
+  const numero = Math.floor(Number(valor));
+  return Number.isSafeInteger(numero) && numero >= 1 ? numero : padrao;
+}
+
+
+const FUSO_RELATORIOS = "America/Sao_Paulo";
+
+function dataLocalAAAAMMDD(data = new Date()) {
+  const partes = Object.fromEntries(
+    new Intl.DateTimeFormat("en-US", {
+      timeZone: FUSO_RELATORIOS,
+      year: "numeric",
+      month: "2-digit",
+      day: "2-digit"
+    })
+      .formatToParts(data)
+      .map(({ type, value }) => [type, value])
+  );
+  return `${partes.year}-${partes.month}-${partes.day}`;
+}
+
 export const AtendimentosController = {
   Ingerir: async function (req, res, next) {
     try {
@@ -65,16 +97,16 @@ export const AtendimentosController = {
 
   Listar: async function (req, res, next) {
     try {
-      const page = Math.max(1, Number(req.query.page) || 1);
-      const pageSize = Math.min(100, Math.max(1, Number(req.query.pageSize) || 20));
+      const page = inteiroPositivoOu(req.query.page, 1);
+      const pageSize = Math.min(100, inteiroPositivoOu(req.query.pageSize, 20));
       const setor = typeof req.query.setor === "string" && req.query.setor.trim() ? req.query.setor.trim() : undefined;
-      const status = req.query.status !== undefined ? Number(req.query.status) : undefined;
+      const status = inteiroOuUndefined(req.query.status);
 
       const result = await listarAtendimentos({
         page,
         pageSize,
         setor,
-        status: Number.isInteger(status) ? status : undefined
+        status
       });
       res.json(result);
     } catch (err) {
@@ -145,8 +177,8 @@ export const AtendimentosController = {
 
   ListarAvaliacoes: async function (req, res, next) {
     try {
-      const page = Math.max(1, Number(req.query.page) || 1);
-      const pageSize = Math.min(100, Math.max(1, Number(req.query.pageSize) || 20));
+      const page = inteiroPositivoOu(req.query.page, 1);
+      const pageSize = Math.min(100, inteiroPositivoOu(req.query.pageSize, 20));
       const filtros = parseFiltrosAvaliacoes(req.query);
 
       const result = await listarAvaliacoes({ page, pageSize, ...filtros });
@@ -190,7 +222,7 @@ export const AtendimentosController = {
       const { setor, dataInicio, dataFim } = parseFiltrosAvaliacoes(req.query);
       const { buffer } = await gerarRankingAtendentesPdf({ setor, dataInicio, dataFim });
 
-      const dataArquivo = new Date().toISOString().slice(0, 10);
+      const dataArquivo = dataLocalAAAAMMDD();
       res.setHeader("Content-Type", "application/pdf");
       res.setHeader("Content-Disposition", `attachment; filename="ranking-atendentes-${dataArquivo}.pdf"`);
       res.setHeader("Content-Length", buffer.length);
@@ -205,7 +237,7 @@ export const AtendimentosController = {
       const filtros = parseFiltrosAvaliacoes(req.query);
       const { buffer } = await gerarRelatorioNaoResolvidosPdf({ ...filtros, resolvido: "nao" });
 
-      const dataArquivo = new Date().toISOString().slice(0, 10);
+      const dataArquivo = dataLocalAAAAMMDD();
       res.setHeader("Content-Type", "application/pdf");
       res.setHeader("Content-Disposition", `attachment; filename="atendimentos-nao-resolvidos-${dataArquivo}.pdf"`);
       res.setHeader("Content-Length", buffer.length);

+ 19 - 10
src/controllers/Auth.Controller.js

@@ -169,20 +169,29 @@ export const AuthController = {
 
       const accessToken = signAccessToken({ sub: usuario.Id, login: usuario.Login, nivel: usuario.Nivel });
 
-      
       const novoRefreshToken = generateRefreshToken();
       const ttlSeconds = stored.RememberMe ? config.jwt.refreshTtlSeconds : config.jwt.refreshTtlShortSeconds;
-      await RefreshToken.query()
-        .patch({ RevokedAt: new Date() })
-        .where({ Id: stored.Id })
-        .whereNull("RevokedAt");
-      await RefreshToken.query().insert({
-        UsuarioId: usuario.Id,
-        TokenHash: hashRefreshToken(novoRefreshToken),
-        ExpiresAt: new Date(Date.now() + ttlSeconds * 1000),
-        RememberMe: stored.RememberMe
+
+      const revogadoNestaRequisicao = await RefreshToken.transaction(async (trx) => {
+        const numUpdated = await RefreshToken.query(trx)
+          .patch({ RevokedAt: new Date() })
+          .where({ Id: stored.Id })
+          .whereNull("RevokedAt");
+        if (numUpdated === 0) return false;
+
+        await RefreshToken.query(trx).insert({
+          UsuarioId: usuario.Id,
+          TokenHash: hashRefreshToken(novoRefreshToken),
+          ExpiresAt: new Date(Date.now() + ttlSeconds * 1000),
+          RememberMe: stored.RememberMe
+        });
+        return true;
       });
 
+      if (!revogadoNestaRequisicao) {
+        throw new AppError("Refresh token inválido ou expirado!", 401);
+      }
+
       return res.status(200).send({ status: true, accessToken, refreshToken: novoRefreshToken });
     } catch (error) {
       return next(error);

+ 6 - 3
src/controllers/Chat.Controller.js

@@ -1,5 +1,6 @@
 import { answerWithContext, answerWithContextStream } from "../chat/chatChain.js";
 import { addMessage, verifyConversationOwner } from "../services/conversationsService.js";
+import { isAdmin } from "../utils/isAdmin.js";
 
 async function tryPersistMessages(conversationId, userId, userContent, result) {
   if (!userId || !conversationId) return;
@@ -24,11 +25,13 @@ export const ChatController = {
         userId: req.userId,
         options: body.options,
         mode: body.mode,
-        isAdmin: String(req.user?.nivel) === "3"
+        isAdmin: isAdmin(req.user)
       });
 
-      await tryPersistMessages(body.conversationId, req.userId, body.message, result);
       res.json(result);
+      tryPersistMessages(body.conversationId, req.userId, body.message, result).catch(
+        (err) => console.error("[chat] falha ao salvar mensagem:", err)
+      );
     } catch (err) {
       next(err);
     }
@@ -54,7 +57,7 @@ export const ChatController = {
         userId: req.userId,
         options: body.options,
         mode: body.mode,
-        isAdmin: String(req.user?.nivel) === "3",
+        isAdmin: isAdmin(req.user),
         signal: abortController.signal,
         onStatus: (stage, count) => {
           const payload = count !== undefined ? { type: "status", stage, count } : { type: "status", stage };

+ 6 - 2
src/controllers/IxcBi.Controller.js

@@ -11,8 +11,12 @@ export const IxcBiController = {
     }
   },
 
-  ListarEndpoints: async function (req, res) {
-    res.json({ endpoints: listarBiPaths() });
+  ListarEndpoints: async function (req, res, next) {
+    try {
+      res.json({ endpoints: listarBiPaths() });
+    } catch (err) {
+      next(err);
+    }
   }
 };
 

+ 2 - 1
src/controllers/Usuario.Controller.js

@@ -4,6 +4,7 @@ import { Usuario } from "../models/Usuario.model.js";
 import { RefreshToken } from "../models/RefreshToken.model.js";
 import { SolicitacaoResetSenha } from "../models/SolicitacaoResetSenha.model.js";
 import { formatarUsuario } from "../utils/formatarUsuario.js";
+import { isAdmin } from "../utils/isAdmin.js";
 
 export const UsuarioController = {
   Listar: async function (req, res, next) {
@@ -102,7 +103,7 @@ export const UsuarioController = {
       if (!id) throw new BadRequestError("ID inválido");
 
       const callerIsOwner = String(req.user?.sub) === String(id);
-      const callerIsAdmin = String(req.user?.nivel) === "3";
+      const callerIsAdmin = isAdmin(req.user);
       if (!callerIsOwner && !callerIsAdmin) {
         throw new AppError("Sem permissão para alterar a senha deste usuário", 403);
       }

+ 3 - 1
src/middleware/requireAdmin.js

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

+ 5 - 0
src/utils/isAdmin.js

@@ -0,0 +1,5 @@
+export const NIVEL_ADMIN = "3";
+
+export function isAdmin(user) {
+  return String(user?.nivel) === NIVEL_ADMIN;
+}