🔒 Implementação de segurança: Login Web fixo e proteção de API

This commit is contained in:
2026-03-21 19:34:46 +00:00
parent 5e8acefa9a
commit 2d3da03ee6
4 changed files with 180 additions and 42 deletions

View File

@@ -457,9 +457,58 @@
grid-template-columns: 1fr;
}
}
/* Login Overlay */
#login-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-base);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
backdrop-filter: blur(20px);
transition: opacity 0.5s ease, visibility 0.5s;
}
#login-overlay.hidden {
opacity: 0;
visibility: hidden;
}
.login-card {
width: 90%;
max-width: 400px;
padding: 2.5rem;
text-align: center;
border: 1px solid var(--accent);
box-shadow: 0 0 30px var(--accent-glow);
}
.login-card h2 {
margin-bottom: 1rem;
font-size: 1.5rem;
background: linear-gradient(135deg, var(--accent), #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
</head>
<body>
<div id="login-overlay">
<div class="card login-card">
<div style="font-size: 3rem; margin-bottom: 1rem;">🔒</div>
<h2>Acesso Restrito</h2>
<p style="color:var(--text-muted); margin-bottom:1.5rem; font-size:0.85rem;">Esta VPS está protegida. Insira a senha mestra para gerenciar o Agente.</p>
<input type="password" id="web-pass-input" class="form-input" placeholder="Senha da VPS" style="margin-bottom:1rem; text-align:center; font-size: 1.1rem; letter-spacing: 0.2rem;" onkeypress="if(event.key==='Enter') attemptLogin()">
<button class="btn btn-primary" style="width:100%; padding: 0.8rem;" onclick="attemptLogin()">Entrar no Dashboard</button>
<div id="login-error" style="color:var(--danger); font-size: 0.75rem; margin-top: 1rem; display: none;">Senha incorreta. Tente novamente.</div>
</div>
</div>
<div class="container">
<header>
<div class="logo">
@@ -590,6 +639,53 @@
<div id="toast">Ação executada!</div>
<script>
let webPassword = localStorage.getItem('vps_web_password') || '';
// Helper para chamadas de API com autenticação
async function apiFetch(url, options = {}) {
if (!options.headers) options.headers = {};
options.headers['X-Web-Password'] = webPassword;
const res = await fetch(url, options);
if (res.status === 401) {
showLoginOverlay();
throw new Error("Não autorizado");
}
return res;
}
function showLoginOverlay() {
document.getElementById('login-overlay').classList.remove('hidden');
document.getElementById('login-error').style.display = 'none';
}
async function attemptLogin() {
const input = document.getElementById('web-pass-input');
const pwd = input.value.trim();
if (!pwd) return;
const res = await fetch('/api/login', {
headers: { 'X-Web-Password': pwd }
});
if (res.ok) {
webPassword = pwd;
localStorage.setItem('vps_web_password', pwd);
document.getElementById('login-overlay').classList.add('hidden');
initDashboard();
} else {
document.getElementById('login-error').style.display = 'block';
input.value = '';
input.focus();
}
}
function initDashboard() {
fetchStats();
loadConfig();
}
// Theme management
function toggleTheme() {
const html = document.documentElement;
const isDark = html.dataset.theme !== 'light';
@@ -609,10 +705,10 @@
updateThemeIcon(savedTheme === 'light');
}
// Stats
async function fetchStats() {
try {
const res = await fetch('/api/status');
if (!res.ok) throw new Error();
const res = await apiFetch('/api/status');
const data = await res.json();
const cpuVal = document.getElementById('cpu-val');
@@ -650,12 +746,12 @@
}
}
setInterval(fetchStats, 3000);
fetchStats();
setInterval(() => { if(!document.getElementById('login-overlay').classList.contains('hidden')) return; fetchStats(); }, 3000);
// Actions
async function executeAction(type) {
const messages = {
reboot_vps: '⚠️ Confirma reboot da VPS?',
reboot_vps: '⚠️ Confirma reboot CRÍTICO da VPS?',
clear_cache: 'Limpar cache do servidor?',
restart_bot: 'Reiniciar o agente AI?'
};
@@ -663,7 +759,7 @@
if (messages[type] && !confirm(messages[type])) return;
try {
const res = await fetch('/api/action', {
const res = await apiFetch('/api/action', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type })
@@ -677,6 +773,7 @@
function showToast(msg, isError = false) {
const toast = document.getElementById('toast');
if(!toast) return;
toast.textContent = msg;
toast.className = isError ? 'error show' : 'show';
setTimeout(() => toast.classList.remove('show'), 3000);
@@ -691,7 +788,7 @@
input.value = '';
try {
const res = await fetch('/api/chat', {
const res = await apiFetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
@@ -709,6 +806,7 @@
function addBubble(text, sender) {
const box = document.getElementById('chat-box');
if(!box) return;
const div = document.createElement('div');
div.className = 'chat-bubble bubble-' + sender;
div.textContent = text;
@@ -718,7 +816,7 @@
async function loadConfig() {
try {
const res = await fetch('/api/config');
const res = await apiFetch('/api/config');
const data = await res.json();
const provider = document.getElementById('active_provider');
const key = document.getElementById('gemini_api_key');
@@ -732,7 +830,7 @@
const key = document.getElementById('gemini_api_key').value.trim();
try {
const res = await fetch('/api/config', {
const res = await apiFetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ active_provider: provider, gemini_api_key: key })
@@ -748,8 +846,6 @@
}
}
window.saveConfiguration = saveConfig;
window.sendMessage = sendChat;
async function testLLMSpeed() {
const btn = document.getElementById('btn-test-llm');
const originalContent = btn.innerHTML;
@@ -757,7 +853,7 @@
btn.disabled = true;
try {
const res = await fetch('/api/test_llm');
const res = await apiFetch('/api/test_llm');
const data = await res.json();
if(data.status === 'success') {
showToast(`✅ LLM Online! Resposta em ${data.latency}s`);
@@ -778,10 +874,28 @@
}
}
window.saveConfiguration = saveConfig;
window.sendMessage = sendChat;
window.executeAction = executeAction;
window.testLLMSpeed = testLLMSpeed;
window.attemptLogin = attemptLogin;
loadConfig();
// Auto-login se já tiver senha salva
if (webPassword) {
(async () => {
try {
const res = await fetch('/api/login', { headers: { 'X-Web-Password': webPassword } });
if (res.ok) {
document.getElementById('login-overlay').classList.add('hidden');
initDashboard();
} else {
showLoginOverlay();
}
} catch(e) { showLoginOverlay(); }
})();
} else {
showLoginOverlay();
}
</script>
</body>
</html>