소스 검색

ajustes pagina de login

leonardo 2 달 전
부모
커밋
24c096d275
3개의 변경된 파일76개의 추가작업 그리고 31개의 파일을 삭제
  1. 1 1
      scripts/evalRetrieval.mjs
  2. 74 29
      src/controllers/Auth.Controller.js
  3. 1 1
      src/middleware/errorHandler.js

+ 1 - 1
scripts/evalRetrieval.mjs

@@ -1,4 +1,4 @@
-).
+
 
 import { config } from "#config/index.js";
 import { searchDocs } from "#services/searchService.js";

+ 74 - 29
src/controllers/Auth.Controller.js

@@ -9,24 +9,49 @@ import { formatarUsuario } from "../utils/formatarUsuario.js";
 const _failedAttempts = new Map();
 const MAX_ATTEMPTS = 5;
 const BLOCK_MS = 15 * 60 * 1000;
+const MAX_TRACKED_KEYS = 10_000;
+const REUSE_GRACE_MS = 60 * 1000;
 
-function recordFailedAttempt(login) {
+
+const DUMMY_HASH = bcrypt.hashSync("oraculo-dummy-password", 10);
+
+setInterval(() => {
+  const now = Date.now();
+  for (const [key, entry] of _failedAttempts) {
+    const expired = entry.blockedUntil > 0 ? entry.blockedUntil <= now : entry.lastAttempt + BLOCK_MS <= now;
+    if (expired) _failedAttempts.delete(key);
+  }
+}, 60_000).unref();
+
+
+function attemptKey(req, login) {
+  return `${req.ip ?? "unknown"}|${login}`;
+}
+
+function recordFailedAttempt(key) {
   const now = Date.now();
-  const entry = _failedAttempts.get(login) ?? { count: 0, blockedUntil: 0 };
+  const entry = _failedAttempts.get(key) ?? { count: 0, blockedUntil: 0, lastAttempt: 0 };
   entry.count += 1;
+  entry.lastAttempt = now;
   if (entry.count >= MAX_ATTEMPTS) {
     entry.blockedUntil = now + BLOCK_MS;
     entry.count = 0;
   }
-  _failedAttempts.set(login, entry);
+  
+  _failedAttempts.delete(key);
+  if (_failedAttempts.size >= MAX_TRACKED_KEYS) {
+    _failedAttempts.delete(_failedAttempts.keys().next().value);
+  }
+  _failedAttempts.set(key, 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 blockedSecondsRemaining(key) {
+  const entry = _failedAttempts.get(key);
+  if (!entry) return 0;
+  const remainingMs = entry.blockedUntil - Date.now();
+  if (remainingMs > 0) return Math.ceil(remainingMs / 1000);
+  if (entry.blockedUntil > 0) _failedAttempts.delete(key);
+  return 0;
 }
 
 export const AuthController = {
@@ -36,31 +61,30 @@ export const AuthController = {
 
       const loginInformado = login.trim();
       const senhaInformada = senha;
+      const chaveTentativa = attemptKey(req, loginInformado);
 
-      if (isLoginBlocked(loginInformado)) {
-        throw new AppError("Muitas tentativas incorretas. Tente novamente em 15 minutos.", 429);
+      const retryAfterSeconds = blockedSecondsRemaining(chaveTentativa);
+      if (retryAfterSeconds > 0) {
+        const err = new AppError("Muitas tentativas incorretas. Tente novamente mais tarde.", 429);
+        err.details = { retryAfterSeconds };
+        throw err;
       }
 
       const usuario = await Usuario.query().findOne(
         loginInformado.includes("@") ? { Email: loginInformado } : { Login: loginInformado }
       );
 
-      if (!usuario) {
-        recordFailedAttempt(loginInformado);
-        throw new AppError("Combinação de usuário e senha inválida!", 401);
-      }
+      
+      const hashSenha = usuario ? String(usuario.Senha ?? "") : DUMMY_HASH;
+      const passwordIsValid = await bcrypt.compare(senhaInformada, hashSenha);
+      const contaInativa = usuario ? String(usuario.Status) === "0" : false;
 
-      if (String(usuario.Status) === "0") {
-        throw new AppError("Usuario inativo!", 401);
-      }
-
-      const passwordIsValid = bcrypt.compareSync(senhaInformada, String(usuario.Senha ?? ""));
-      if (!passwordIsValid) {
-        recordFailedAttempt(loginInformado);
+      if (!usuario || !passwordIsValid || contaInativa) {
+        recordFailedAttempt(chaveTentativa);
         throw new AppError("Combinação de usuário e senha inválida!", 401);
       }
 
-      _failedAttempts.delete(loginInformado);
+      _failedAttempts.delete(chaveTentativa);
 
       let accessToken = null;
       let refreshToken = null;
@@ -92,12 +116,21 @@ export const AuthController = {
     try {
       const { refreshToken } = req.body;
       const tokenHash = hashRefreshToken(refreshToken);
-      const stored = await RefreshToken.query()
-        .findOne({ TokenHash: tokenHash })
-        .whereNull("RevokedAt")
-        .where("ExpiresAt", ">", new Date());
+      const stored = await RefreshToken.query().findOne({ TokenHash: tokenHash });
 
-      if (!stored) {
+      if (!stored || new Date(stored.ExpiresAt) <= new Date()) {
+        throw new AppError("Refresh token inválido ou expirado!", 401);
+      }
+
+      if (stored.RevokedAt) {
+        
+        const revokedThereMs = Date.now() - new Date(stored.RevokedAt).getTime();
+        if (revokedThereMs > REUSE_GRACE_MS) {
+          await RefreshToken.query()
+            .patch({ RevokedAt: new Date() })
+            .where({ UsuarioId: stored.UsuarioId })
+            .whereNull("RevokedAt");
+        }
         throw new AppError("Refresh token inválido ou expirado!", 401);
       }
 
@@ -108,7 +141,19 @@ export const AuthController = {
 
       const accessToken = signAccessToken({ sub: usuario.Id, login: usuario.Login, nivel: usuario.Nivel });
 
-      return res.status(200).send({ status: true, accessToken });
+      
+      const novoRefreshToken = generateRefreshToken();
+      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() + config.jwt.refreshTtlSeconds * 1000)
+      });
+
+      return res.status(200).send({ status: true, accessToken, refreshToken: novoRefreshToken });
     } catch (error) {
       return next(error);
     }

+ 1 - 1
src/middleware/errorHandler.js

@@ -7,7 +7,7 @@ export function errorHandler(err, _req, res, _next) {
   }
 
   if (err instanceof AppError) {
-    return res.status(err.statusCode).json({ error: err.message });
+    return res.status(err.statusCode).json({ error: err.message, ...(err.details ?? {}) });
   }
 
   const status = Number(err?.statusCode ?? err?.status ?? 500);