|
|
@@ -1,133 +1,16 @@
|
|
|
-import bcrypt from "bcryptjs";
|
|
|
-import { AppError } from "../shared/errors/index.js";
|
|
|
-import { Usuario } from "../models/Usuario.model.js";
|
|
|
-import { RefreshToken } from "../models/RefreshToken.model.js";
|
|
|
-import { SolicitacaoResetSenha } from "../models/SolicitacaoResetSenha.model.js";
|
|
|
-import { signAccessToken, generateRefreshToken, hashRefreshToken } from "../services/authTokens.js";
|
|
|
-import { config } from "../config/index.js";
|
|
|
+import * as authService from "../services/authService.js";
|
|
|
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;
|
|
|
-
|
|
|
-const _resetAttempts = new Map();
|
|
|
-const RESET_MAX = 5;
|
|
|
-const RESET_WINDOW_MS = 15 * 60 * 1000;
|
|
|
-
|
|
|
-
|
|
|
-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);
|
|
|
- }
|
|
|
- for (const [ip, entry] of _resetAttempts) {
|
|
|
- if (entry.windowStart + RESET_WINDOW_MS <= now) _resetAttempts.delete(ip);
|
|
|
- }
|
|
|
-}, 60_000).unref();
|
|
|
-
|
|
|
-
|
|
|
-function attemptKey(req, login) {
|
|
|
- return `${req.ip ?? "unknown"}|${login}`;
|
|
|
-}
|
|
|
-
|
|
|
-function recordFailedAttempt(key) {
|
|
|
- const now = Date.now();
|
|
|
- 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.delete(key);
|
|
|
- if (_failedAttempts.size >= MAX_TRACKED_KEYS) {
|
|
|
- _failedAttempts.delete(_failedAttempts.keys().next().value);
|
|
|
- }
|
|
|
- _failedAttempts.set(key, entry);
|
|
|
-}
|
|
|
-
|
|
|
-function resetRetryAfterSeconds(ip) {
|
|
|
- const now = Date.now();
|
|
|
- const entry = _resetAttempts.get(ip);
|
|
|
- if (!entry || now - entry.windowStart > RESET_WINDOW_MS) {
|
|
|
- if (_resetAttempts.size >= MAX_TRACKED_KEYS) {
|
|
|
- _resetAttempts.delete(_resetAttempts.keys().next().value);
|
|
|
- }
|
|
|
- _resetAttempts.set(ip, { count: 1, windowStart: now });
|
|
|
- return 0;
|
|
|
- }
|
|
|
- entry.count += 1;
|
|
|
- if (entry.count > RESET_MAX) {
|
|
|
- return Math.ceil((entry.windowStart + RESET_WINDOW_MS - now) / 1000);
|
|
|
- }
|
|
|
- return 0;
|
|
|
-}
|
|
|
-
|
|
|
-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 = {
|
|
|
Login: async function (req, res, next) {
|
|
|
try {
|
|
|
const { login, senha, rememberMe } = req.body;
|
|
|
-
|
|
|
- const loginInformado = login.trim();
|
|
|
- const senhaInformada = senha;
|
|
|
- const chaveTentativa = attemptKey(req, loginInformado);
|
|
|
-
|
|
|
- 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 }
|
|
|
- );
|
|
|
-
|
|
|
-
|
|
|
- const hashSenha = usuario ? String(usuario.Senha ?? "") : DUMMY_HASH;
|
|
|
- const passwordIsValid = await bcrypt.compare(senhaInformada, hashSenha);
|
|
|
- const contaInativa = usuario ? String(usuario.Status) === "0" : false;
|
|
|
-
|
|
|
- if (!usuario || !passwordIsValid || contaInativa) {
|
|
|
- recordFailedAttempt(chaveTentativa);
|
|
|
- throw new AppError("Combinação de usuário e senha inválida!", 401);
|
|
|
- }
|
|
|
-
|
|
|
- _failedAttempts.delete(chaveTentativa);
|
|
|
-
|
|
|
- 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 ttlSeconds = rememberMe ? config.jwt.refreshTtlSeconds : config.jwt.refreshTtlShortSeconds;
|
|
|
- const expiresAt = new Date(Date.now() + ttlSeconds * 1000);
|
|
|
- await RefreshToken.query().insert({
|
|
|
- UsuarioId: usuario.Id,
|
|
|
- TokenHash: tokenHash,
|
|
|
- ExpiresAt: expiresAt,
|
|
|
- RememberMe: Boolean(rememberMe)
|
|
|
- });
|
|
|
- }
|
|
|
+ const { usuario, accessToken, refreshToken } = await authService.login({
|
|
|
+ login,
|
|
|
+ senha,
|
|
|
+ rememberMe,
|
|
|
+ ip: req.ip
|
|
|
+ });
|
|
|
|
|
|
return res.status(200).send({
|
|
|
status: true,
|
|
|
@@ -142,57 +25,10 @@ export const AuthController = {
|
|
|
|
|
|
Refresh: async function (req, res, next) {
|
|
|
try {
|
|
|
- const { refreshToken } = req.body;
|
|
|
- const tokenHash = hashRefreshToken(refreshToken);
|
|
|
- const stored = await RefreshToken.query().findOne({ TokenHash: tokenHash });
|
|
|
-
|
|
|
- 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);
|
|
|
- }
|
|
|
-
|
|
|
- const usuario = await Usuario.query().findById(stored.UsuarioId);
|
|
|
- if (!usuario || String(usuario.Status) === "0") {
|
|
|
- throw new AppError("Usuario inativo!", 401);
|
|
|
- }
|
|
|
-
|
|
|
- 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;
|
|
|
-
|
|
|
- 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;
|
|
|
+ const { accessToken, refreshToken } = await authService.refresh({
|
|
|
+ refreshToken: req.body.refreshToken
|
|
|
});
|
|
|
-
|
|
|
- if (!revogadoNestaRequisicao) {
|
|
|
- throw new AppError("Refresh token inválido ou expirado!", 401);
|
|
|
- }
|
|
|
-
|
|
|
- return res.status(200).send({ status: true, accessToken, refreshToken: novoRefreshToken });
|
|
|
+ return res.status(200).send({ status: true, accessToken, refreshToken });
|
|
|
} catch (error) {
|
|
|
return next(error);
|
|
|
}
|
|
|
@@ -200,31 +36,7 @@ export const AuthController = {
|
|
|
|
|
|
SolicitarResetSenha: async function (req, res, next) {
|
|
|
try {
|
|
|
- const loginInformado = req.body.login.trim();
|
|
|
-
|
|
|
- const retryAfterSeconds = resetRetryAfterSeconds(req.ip ?? "unknown");
|
|
|
- if (retryAfterSeconds > 0) {
|
|
|
- const err = new AppError("Muitas solicitações. Tente novamente mais tarde.", 429);
|
|
|
- err.details = { retryAfterSeconds };
|
|
|
- throw err;
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- const usuario = await Usuario.query()
|
|
|
- .where(loginInformado.includes("@") ? { Email: loginInformado } : { Login: loginInformado })
|
|
|
- .orderBy("Id")
|
|
|
- .first();
|
|
|
-
|
|
|
-
|
|
|
- if (usuario && String(usuario.Status) !== "0") {
|
|
|
- const pendente = await SolicitacaoResetSenha.query().findOne({
|
|
|
- UsuarioId: usuario.Id,
|
|
|
- Status: "pendente"
|
|
|
- });
|
|
|
- if (!pendente) {
|
|
|
- await SolicitacaoResetSenha.query().insert({ UsuarioId: usuario.Id, Status: "pendente" });
|
|
|
- }
|
|
|
- }
|
|
|
+ await authService.solicitarResetSenha({ login: req.body.login, ip: req.ip });
|
|
|
|
|
|
return res.status(200).send({
|
|
|
status: true,
|
|
|
@@ -237,14 +49,7 @@ export const AuthController = {
|
|
|
|
|
|
Logout: async function (req, res, next) {
|
|
|
try {
|
|
|
- const { refreshToken } = req.body ?? {};
|
|
|
- if (refreshToken) {
|
|
|
- const tokenHash = hashRefreshToken(refreshToken);
|
|
|
- await RefreshToken.query()
|
|
|
- .patch({ RevokedAt: new Date() })
|
|
|
- .where({ TokenHash: tokenHash })
|
|
|
- .whereNull("RevokedAt");
|
|
|
- }
|
|
|
+ await authService.logout({ refreshToken: req.body?.refreshToken });
|
|
|
return res.status(200).send({ status: true, msg: "Logout realizado com sucesso!" });
|
|
|
} catch (error) {
|
|
|
return next(error);
|