🔒 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

@@ -17,7 +17,8 @@ def get_config():
# Configuração Padrão
return {
"active_provider": "gemini",
"gemini_api_key": ""
"gemini_api_key": "",
"web_password": "@@Gi05Br;;"
}
def save_config(cfg):

64
main.py
View File

@@ -1,11 +1,16 @@
import os
import psutil
from fastapi import FastAPI, Request
import subprocess
import time
import json
from fastapi import FastAPI, Request, Header, Depends, HTTPException, status
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from dotenv import load_dotenv
from starlette.concurrency import run_in_threadpool
from ai_agent import query_agent
from config import get_config, save_config
# Carrega as variáveis do .env
load_dotenv()
@@ -13,22 +18,37 @@ load_dotenv()
app = FastAPI(title="VpsTelegramBot API")
# Configura templates HTML
# Certifique-se de que a pasta 'templates' existe e tem o index.html
templates = Jinja2Templates(directory="templates")
@app.get("/favicon.ico", include_in_schema=False)
async def favicon():
return JSONResponse(content={"status": "ok"})
# --- SEGURANÇA ---
async def verify_password(x_web_password: str = Header(None)):
cfg = get_config()
saved_pwd = cfg.get("web_password", "@@Gi05Br;;")
if not x_web_password or x_web_password != saved_pwd:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Senha Web inválida ou ausente."
)
return True
# --- ROTAS PÚBLICAS ---
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
"""Renderiza o Dashboard Web."""
return templates.TemplateResponse("index.html", {"request": request})
from starlette.concurrency import run_in_threadpool
@app.get("/favicon.ico", include_in_schema=False)
async def favicon():
"""Favicon dummy para evitar 404."""
return JSONResponse(content={"status": "ok"})
# --- ROTAS PROTEGIDAS (API) ---
@app.get("/api/login")
async def check_login(is_auth: bool = Depends(verify_password)):
return {"status": "success"}
@app.get("/api/status")
async def get_system_status():
async def get_system_status(is_auth: bool = Depends(verify_password)):
"""Retorna o status do sistema (CPU, RAM, Disco) sem travar o loop."""
def get_stats():
cpu_percent = psutil.cpu_percent(interval=0.1)
@@ -47,24 +67,20 @@ async def get_system_status():
"percent": disk.percent
}
}
data = await run_in_threadpool(get_stats)
return JSONResponse(content=data)
import subprocess
from config import get_config, save_config
@app.get("/api/config")
async def read_configuration():
async def read_configuration(is_auth: bool = Depends(verify_password)):
return JSONResponse(content=get_config())
@app.post("/api/config")
async def update_configuration(req: dict):
async def update_configuration(req: dict, is_auth: bool = Depends(verify_password)):
save_config(req)
return JSONResponse(content={"status": "success"})
@app.post("/api/action")
async def execute_smart_action(action: dict):
async def execute_smart_action(action: dict, is_auth: bool = Depends(verify_password)):
"""Executa ações predefinidas no servidor (Smart Actions da Web UI)."""
action_type = action.get("type")
@@ -72,37 +88,31 @@ async def execute_smart_action(action: dict):
return JSONResponse(content={"status": "success", "message": "Pong! Servidor online e responsivo."})
elif action_type == "restart_bot":
# Dá um pequeno delay e depois reinicia o próprio container a partir de fora (pelo host docker)
subprocess.Popen("sleep 1 && docker restart vps-ai-agent", shell=True)
return JSONResponse(content={"status": "success", "message": "Reboot do Agente autorizado. Estará de volta em instantes!"})
return JSONResponse(content={"status": "success", "message": "Reboot do Agente autorizado."})
elif action_type == "clear_cache":
# Roda um docker prune para deletar volumes perdidos e todos containers parados (limpeza profunda)
subprocess.Popen("docker system prune -af --volumes", shell=True)
return JSONResponse(content={"status": "success", "message": "Limpando caches obsoletos em background! Verifique o gráfico de disco em instantes."})
return JSONResponse(content={"status": "success", "message": "Limpando caches obsoletos em background!"})
elif action_type == "reboot_vps":
# Hacker trick: Roda um container hiper-privilegiado descartável pra entrar no espaço do host (PID 1) e emitir comando de REBOOT físico
subprocess.Popen("sleep 2 && docker run --rm --privileged --pid=host alpine nsenter -t 1 -m -u -n -i reboot", shell=True)
return JSONResponse(content={"status": "success", "message": "🚨 O REBOOT CRÍTICO COMEÇOU. A VPS inteira desligará e religará agora."})
return JSONResponse(content={"status": "success", "message": "🚨 O REBOOT CRÍTICO COMEÇOU."})
return JSONResponse(content={"status": "error", "message": "Ação desconhecida."}, status_code=400)
@app.post("/api/chat")
async def web_chat(message: dict):
async def web_chat(message: dict, is_auth: bool = Depends(verify_password)):
"""Endpoint para interagir com a IA via Web UI."""
user_text = message.get("text", "")
if not user_text:
return JSONResponse(content={"reply": "Por favor, digite um comando válido."})
# Executa a IA em uma thread separada para não travar a UI/API de status
reply = await run_in_threadpool(query_agent, prompt=user_text)
return JSONResponse(content={"reply": reply})
import time
@app.get("/api/test_llm")
async def test_llm_speed():
async def test_llm_speed(is_auth: bool = Depends(verify_password)):
"""Mede a velocidade de resposta da IA ativa."""
start_time = time.time()
try:
@@ -116,11 +126,9 @@ async def test_llm_speed():
async def telegram_webhook(request: Request):
"""Recebe as atualizações (mensagens) do Telegram."""
update = await request.json()
# O bot_logic.py lidará com o 'update' no futuro
print("Update recebido do Telegram:", update)
return {"ok": True}
if __name__ == "__main__":
import uvicorn
# Executa o servidor na porta 8000 acessível de qualquer lugar na rede
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

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>

15
update.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
# Script de deploy e sincronização automática
echo "🚀 Iniciando deploy e push para o Gitea..."
# 1. Build do Docker
echo "📦 Reconstruindo container..."
docker compose down && docker compose build && docker compose up -d
# 2. Sincronização com Git (Gitea)
echo "git Pushing para o Gitea..."
git add .
git commit -m "🔒 Implementação de segurança: Login Web fixo e proteção de API"
git push origin master
echo "✅ Sucesso! Agente atualizado e código no Gitea."