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:
2026-06-17 10:02:59 -03:00
commit ae629d1dc2
50 changed files with 17137 additions and 0 deletions
+185
View File
@@ -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;