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,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>
|
||||
Reference in New Issue
Block a user