Migração para PostgreSQL multi-driver + correções de segurança
- Camada de banco unificada (src/database.js): drivers Postgres/Firebird, tradutor de SQL, suporte a schema e pool de conexões - Conexões: novo_local (Postgres externo) e firebird_local (legado) - Tela de rotas da API redesenhada (auth, params, exemplos de body) - Correções de segurança (críticos/altos/médios/baixos): XSS no chat, escalonamento de privilégio, mídia autenticada, SQL restrito a gerente, JWT sem fallback + issuer, IDOR em conversas, CORS por allowlist, rate-limit no login, limites de corpo por rota - Deploy alinhado: install.sh grava .env com PG_*, migracoes.js driver-aware Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const path = require('path');
|
||||
const db = require('../database');
|
||||
const authConfig = require('../config/auth');
|
||||
|
||||
// ── Rate limit de login (em memória) — proteção anti força bruta ──
|
||||
const LOGIN_MAX = 10; // tentativas falhas permitidas
|
||||
const LOGIN_JANELA_MS = 15 * 60 * 1000; // por janela de 15 minutos
|
||||
const loginTentativas = new Map();
|
||||
function chaveLogin(req, login) { return (req.ip || '') + '|' + (login || ''); }
|
||||
function loginBloqueado(req, login) {
|
||||
const e = loginTentativas.get(chaveLogin(req, login));
|
||||
if (!e) return false;
|
||||
if (Date.now() - e.inicio > LOGIN_JANELA_MS) { loginTentativas.delete(chaveLogin(req, login)); return false; }
|
||||
return e.count >= LOGIN_MAX;
|
||||
}
|
||||
function registrarFalhaLogin(req, login) {
|
||||
const k = chaveLogin(req, login);
|
||||
const agora = Date.now();
|
||||
const e = loginTentativas.get(k);
|
||||
if (!e || agora - e.inicio > LOGIN_JANELA_MS) loginTentativas.set(k, { count: 1, inicio: agora });
|
||||
else e.count++;
|
||||
}
|
||||
function limparTentativasLogin(req, login) { loginTentativas.delete(chaveLogin(req, login)); }
|
||||
|
||||
class AuthController {
|
||||
/**
|
||||
* Exibe a página de login
|
||||
* Endpoint: GET /app/:alias/login
|
||||
*/
|
||||
static async loginPage(req, res) {
|
||||
const loginPath = path.resolve(__dirname, '../public/login.html');
|
||||
res.sendFile(loginPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login do usuário no aplicativo
|
||||
* Endpoint: POST /app/:alias/login
|
||||
* Body: { USU_LOGIN, USU_SENHA }
|
||||
*/
|
||||
static async login(req, res) {
|
||||
try {
|
||||
const { alias } = req.params;
|
||||
const { USU_LOGIN, USU_SENHA } = req.body;
|
||||
|
||||
// Validação dos campos obrigatórios
|
||||
if (!USU_LOGIN || !USU_SENHA) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Os campos USU_LOGIN e USU_SENHA são obrigatórios.',
|
||||
});
|
||||
}
|
||||
|
||||
if (loginBloqueado(req, USU_LOGIN)) {
|
||||
return res.status(429).json({
|
||||
success: false,
|
||||
error: 'Muitas tentativas de login. Aguarde alguns minutos e tente novamente.',
|
||||
});
|
||||
}
|
||||
|
||||
// Busca o usuário pelo login
|
||||
const sql = `
|
||||
SELECT
|
||||
USU_CODIGO_ID,
|
||||
USU_NIVEL_ID,
|
||||
USU_NOME,
|
||||
USU_LOGIN,
|
||||
USU_EMAIL,
|
||||
USU_STATUS,
|
||||
USU_ACESSO_WEB,
|
||||
USU_CAMINHO_FOTO,
|
||||
USU_DT_ULTIMO_ACESSO,
|
||||
USU_TIPO,
|
||||
USU_TIPO_CHAT
|
||||
FROM USUARIOS
|
||||
WHERE USU_LOGIN = ?
|
||||
AND (USU_SENHA = ? OR USU_SENHA_WEB = ?)
|
||||
AND COALESCE(USU_ACESSO_WEB, 0) = 1
|
||||
AND USU_STATUS = 'A'
|
||||
`;
|
||||
|
||||
const result = await db.query(alias, sql, [USU_LOGIN, USU_SENHA, USU_SENHA]);
|
||||
|
||||
if (result.length === 0) {
|
||||
registrarFalhaLogin(req, USU_LOGIN);
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Credenciais inválidas ou usuário sem permissão de acesso.',
|
||||
});
|
||||
}
|
||||
|
||||
// Login bem-sucedido: zera o contador de tentativas
|
||||
limparTentativasLogin(req, USU_LOGIN);
|
||||
|
||||
const user = result[0];
|
||||
|
||||
// Busca as empresas que o usuário tem acesso
|
||||
const empresas = await db.query(alias,
|
||||
'SELECT USE_EMPRESA_ID FROM USUARIOS_EMPRESA WHERE USE_USUARIO_ID = ?',
|
||||
[user.USU_CODIGO_ID]
|
||||
);
|
||||
const empresasIds = empresas.map(e => e.USE_EMPRESA_ID);
|
||||
|
||||
// Atualiza a data do último acesso
|
||||
await db.execute(
|
||||
alias,
|
||||
`UPDATE USUARIOS SET USU_DT_ULTIMO_ACESSO = CURRENT_DATE WHERE USU_CODIGO_ID = ?`,
|
||||
[user.USU_CODIGO_ID]
|
||||
);
|
||||
|
||||
// Dados do usuário para o token
|
||||
const payload = {
|
||||
id: user.USU_CODIGO_ID,
|
||||
nivelId: user.USU_NIVEL_ID,
|
||||
nome: user.USU_NOME.trim(),
|
||||
login: user.USU_LOGIN.trim(),
|
||||
email: user.USU_EMAIL ? user.USU_EMAIL.trim() : null,
|
||||
tipo: user.USU_TIPO ? user.USU_TIPO.trim() : null,
|
||||
tipoChat: user.USU_TIPO_CHAT ? user.USU_TIPO_CHAT.trim() : null,
|
||||
alias,
|
||||
empresas: empresasIds, // empresas que o usuário pode acessar
|
||||
};
|
||||
|
||||
// Gera o token JWT
|
||||
const token = jwt.sign(payload, authConfig.secret, {
|
||||
expiresIn: authConfig.expiresIn,
|
||||
issuer: authConfig.issuer,
|
||||
});
|
||||
|
||||
// Retorna o token e dados do usuário
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Login realizado com sucesso.',
|
||||
alias,
|
||||
token,
|
||||
expiresIn: authConfig.expiresIn,
|
||||
user: {
|
||||
...payload,
|
||||
foto: user.USU_CAMINHO_FOTO ? user.USU_CAMINHO_FOTO.trim() : null,
|
||||
ultimoAcesso: user.USU_DT_ULTIMO_ACESSO,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Erro no login:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: `Erro interno ao realizar login: ${err.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida o token atual e retorna dados atualizados do usuário
|
||||
* Endpoint: GET /app/:alias/me
|
||||
*/
|
||||
static async me(req, res) {
|
||||
try {
|
||||
return res.json({
|
||||
success: true,
|
||||
alias: req.params.alias,
|
||||
user: {
|
||||
...req.user,
|
||||
authType: req.authType || 'jwt',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Erro ao buscar usuário:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: `Erro ao buscar dados do usuário: ${err.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard do app
|
||||
* Endpoint: GET /app/:alias/dashboard
|
||||
*/
|
||||
static async dashboard(req, res) {
|
||||
const dashboardPath = path.resolve(__dirname, '../public/dashboard.html');
|
||||
res.sendFile(dashboardPath);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AuthController;
|
||||
Reference in New Issue
Block a user