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
+208
View File
@@ -0,0 +1,208 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Avalie seu Atendimento</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: #fff;
border-radius: 20px;
padding: 40px;
max-width: 480px;
width: 100%;
text-align: center;
box-shadow: 0 20px 60px rgba(0,0,0,0.15);
}
.logo { font-size: 48px; margin-bottom: 16px; }
h1 { font-size: 24px; color: #1f2937; margin-bottom: 8px; }
.subtitle { font-size: 14px; color: #6b7280; margin-bottom: 32px; }
.stars {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 32px;
direction: rtl;
}
.stars input { display: none; }
.stars label {
font-size: 48px;
cursor: pointer;
color: #d1d5db;
transition: color .2s, transform .15s;
}
.stars label:hover,
.stars label:hover ~ label,
.stars input:checked ~ label {
color: #f59e0b;
transform: scale(1.1);
}
.stars input:checked + label {
color: #f59e0b;
}
textarea {
width: 100%;
padding: 14px 16px;
border: 2px solid #e5e7eb;
border-radius: 12px;
font-size: 14px;
font-family: inherit;
resize: vertical;
min-height: 80px;
outline: none;
transition: border-color .2s;
margin-bottom: 20px;
}
textarea:focus { border-color: #667eea; }
.btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: opacity .2s;
}
.btn:hover { opacity: .9; }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.rating-text {
font-size: 14px;
color: #6b7280;
margin-bottom: 24px;
min-height: 20px;
}
.success { display: none; }
.success .icon { font-size: 64px; margin-bottom: 16px; }
.success h2 { color: #059669; margin-bottom: 8px; }
.success p { color: #6b7280; }
.erro {
color: #ef4444;
font-size: 14px;
margin-top: 12px;
display: none;
}
</style>
</head>
<body>
<div class="container" id="app">
<div class="logo">💬</div>
<h1>Avalie seu Atendimento</h1>
<p class="subtitle">Sua opinião é muito importante para melhorarmos nosso serviço</p>
<div class="rating-text" id="ratingText">Toque nas estrelas para avaliar</div>
<div class="stars" id="starContainer">
<input type="radio" name="star" id="star5" value="5">
<label for="star5" title="Excelente"></label>
<input type="radio" name="star" id="star4" value="4">
<label for="star4" title="Bom"></label>
<input type="radio" name="star" id="star3" value="3">
<label for="star3" title="Regular"></label>
<input type="radio" name="star" id="star2" value="2">
<label for="star2" title="Ruim"></label>
<input type="radio" name="star" id="star1" value="1">
<label for="star1" title="Péssimo"></label>
</div>
<textarea id="comentario" placeholder="Deixe seu comentário (opcional)..."></textarea>
<button class="btn" id="btnEnviar" onclick="enviar()">Enviar Avaliação</button>
<div class="erro" id="erro"></div>
<div class="success" id="success">
<div class="icon"></div>
<h2>Agradecemos sua avaliação!</h2>
<p>Seu feedback nos ajuda a melhorar cada vez mais.</p>
</div>
</div>
<script>
var alias, conversaId, empresaId;
function getParams() {
var params = new URLSearchParams(window.location.search);
alias = params.get('alias');
conversaId = params.get('conversa');
empresaId = params.get('empresa');
if (!alias || !conversaId || !empresaId) {
document.getElementById('erro').textContent = 'Link inválido. Entre em contato conosco.';
document.getElementById('erro').style.display = 'block';
document.querySelector('.stars').style.display = 'none';
document.querySelector('textarea').style.display = 'none';
document.getElementById('btnEnviar').style.display = 'none';
}
}
var nota = 0;
document.querySelectorAll('.stars input').forEach(function(input) {
input.addEventListener('change', function() {
nota = parseInt(this.value);
var textos = ['', 'Péssimo', 'Ruim', 'Regular', 'Bom', 'Excelente!'];
document.getElementById('ratingText').textContent = textos[nota] || '';
});
});
async function enviar() {
if (nota === 0) {
document.getElementById('erro').textContent = 'Selecione uma avaliação de 1 a 5 estrelas.';
document.getElementById('erro').style.display = 'block';
return;
}
var btn = document.getElementById('btnEnviar');
btn.disabled = true;
btn.textContent = 'Enviando...';
document.getElementById('erro').style.display = 'none';
try {
var resp = await fetch('/api/' + alias + '/csat/avaliar', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversaId: parseInt(conversaId),
empresaId: parseInt(empresaId),
nota: nota,
comentario: document.getElementById('comentario').value.trim()
})
});
var data = await resp.json();
if (data.success) {
document.getElementById('app').querySelector('.stars').style.display = 'none';
document.querySelector('textarea').style.display = 'none';
document.getElementById('btnEnviar').style.display = 'none';
document.getElementById('ratingText').style.display = 'none';
document.getElementById('success').style.display = 'block';
} else {
throw new Error(data.error || 'Erro ao enviar');
}
} catch(e) {
document.getElementById('erro').textContent = 'Erro ao enviar: ' + e.message;
document.getElementById('erro').style.display = 'block';
btn.disabled = false;
btn.textContent = 'Enviar Avaliação';
}
}
getParams();
</script>
</body>
</html>