🔒 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 # Configuração Padrão
return { return {
"active_provider": "gemini", "active_provider": "gemini",
"gemini_api_key": "" "gemini_api_key": "",
"web_password": "@@Gi05Br;;"
} }
def save_config(cfg): def save_config(cfg):

64
main.py
View File

@@ -1,11 +1,16 @@
import os import os
import psutil 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.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from dotenv import load_dotenv from dotenv import load_dotenv
from starlette.concurrency import run_in_threadpool
from ai_agent import query_agent from ai_agent import query_agent
from config import get_config, save_config
# Carrega as variáveis do .env # Carrega as variáveis do .env
load_dotenv() load_dotenv()
@@ -13,22 +18,37 @@ load_dotenv()
app = FastAPI(title="VpsTelegramBot API") app = FastAPI(title="VpsTelegramBot API")
# Configura templates HTML # Configura templates HTML
# Certifique-se de que a pasta 'templates' existe e tem o index.html
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
@app.get("/favicon.ico", include_in_schema=False) # --- SEGURANÇA ---
async def favicon(): async def verify_password(x_web_password: str = Header(None)):
return JSONResponse(content={"status": "ok"}) 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) @app.get("/", response_class=HTMLResponse)
async def read_root(request: Request): async def read_root(request: Request):
"""Renderiza o Dashboard Web.""" """Renderiza o Dashboard Web."""
return templates.TemplateResponse("index.html", {"request": request}) 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") @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.""" """Retorna o status do sistema (CPU, RAM, Disco) sem travar o loop."""
def get_stats(): def get_stats():
cpu_percent = psutil.cpu_percent(interval=0.1) cpu_percent = psutil.cpu_percent(interval=0.1)
@@ -47,24 +67,20 @@ async def get_system_status():
"percent": disk.percent "percent": disk.percent
} }
} }
data = await run_in_threadpool(get_stats) data = await run_in_threadpool(get_stats)
return JSONResponse(content=data) return JSONResponse(content=data)
import subprocess
from config import get_config, save_config
@app.get("/api/config") @app.get("/api/config")
async def read_configuration(): async def read_configuration(is_auth: bool = Depends(verify_password)):
return JSONResponse(content=get_config()) return JSONResponse(content=get_config())
@app.post("/api/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) save_config(req)
return JSONResponse(content={"status": "success"}) return JSONResponse(content={"status": "success"})
@app.post("/api/action") @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).""" """Executa ações predefinidas no servidor (Smart Actions da Web UI)."""
action_type = action.get("type") 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."}) return JSONResponse(content={"status": "success", "message": "Pong! Servidor online e responsivo."})
elif action_type == "restart_bot": 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) 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": 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) 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": 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) 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) return JSONResponse(content={"status": "error", "message": "Ação desconhecida."}, status_code=400)
@app.post("/api/chat") @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.""" """Endpoint para interagir com a IA via Web UI."""
user_text = message.get("text", "") user_text = message.get("text", "")
if not user_text: if not user_text:
return JSONResponse(content={"reply": "Por favor, digite um comando válido."}) 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) reply = await run_in_threadpool(query_agent, prompt=user_text)
return JSONResponse(content={"reply": reply}) return JSONResponse(content={"reply": reply})
import time
@app.get("/api/test_llm") @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.""" """Mede a velocidade de resposta da IA ativa."""
start_time = time.time() start_time = time.time()
try: try:
@@ -116,11 +126,9 @@ async def test_llm_speed():
async def telegram_webhook(request: Request): async def telegram_webhook(request: Request):
"""Recebe as atualizações (mensagens) do Telegram.""" """Recebe as atualizações (mensagens) do Telegram."""
update = await request.json() update = await request.json()
# O bot_logic.py lidará com o 'update' no futuro
print("Update recebido do Telegram:", update) print("Update recebido do Telegram:", update)
return {"ok": True} return {"ok": True}
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn 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) uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

View File

@@ -457,9 +457,58 @@
grid-template-columns: 1fr; 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> </style>
</head> </head>
<body> <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"> <div class="container">
<header> <header>
<div class="logo"> <div class="logo">
@@ -590,6 +639,53 @@
<div id="toast">Ação executada!</div> <div id="toast">Ação executada!</div>
<script> <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() { function toggleTheme() {
const html = document.documentElement; const html = document.documentElement;
const isDark = html.dataset.theme !== 'light'; const isDark = html.dataset.theme !== 'light';
@@ -609,10 +705,10 @@
updateThemeIcon(savedTheme === 'light'); updateThemeIcon(savedTheme === 'light');
} }
// Stats
async function fetchStats() { async function fetchStats() {
try { try {
const res = await fetch('/api/status'); const res = await apiFetch('/api/status');
if (!res.ok) throw new Error();
const data = await res.json(); const data = await res.json();
const cpuVal = document.getElementById('cpu-val'); const cpuVal = document.getElementById('cpu-val');
@@ -650,12 +746,12 @@
} }
} }
setInterval(fetchStats, 3000); setInterval(() => { if(!document.getElementById('login-overlay').classList.contains('hidden')) return; fetchStats(); }, 3000);
fetchStats();
// Actions
async function executeAction(type) { async function executeAction(type) {
const messages = { const messages = {
reboot_vps: '⚠️ Confirma reboot da VPS?', reboot_vps: '⚠️ Confirma reboot CRÍTICO da VPS?',
clear_cache: 'Limpar cache do servidor?', clear_cache: 'Limpar cache do servidor?',
restart_bot: 'Reiniciar o agente AI?' restart_bot: 'Reiniciar o agente AI?'
}; };
@@ -663,7 +759,7 @@
if (messages[type] && !confirm(messages[type])) return; if (messages[type] && !confirm(messages[type])) return;
try { try {
const res = await fetch('/api/action', { const res = await apiFetch('/api/action', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type }) body: JSON.stringify({ type })
@@ -677,6 +773,7 @@
function showToast(msg, isError = false) { function showToast(msg, isError = false) {
const toast = document.getElementById('toast'); const toast = document.getElementById('toast');
if(!toast) return;
toast.textContent = msg; toast.textContent = msg;
toast.className = isError ? 'error show' : 'show'; toast.className = isError ? 'error show' : 'show';
setTimeout(() => toast.classList.remove('show'), 3000); setTimeout(() => toast.classList.remove('show'), 3000);
@@ -691,7 +788,7 @@
input.value = ''; input.value = '';
try { try {
const res = await fetch('/api/chat', { const res = await apiFetch('/api/chat', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }) body: JSON.stringify({ text })
@@ -709,6 +806,7 @@
function addBubble(text, sender) { function addBubble(text, sender) {
const box = document.getElementById('chat-box'); const box = document.getElementById('chat-box');
if(!box) return;
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'chat-bubble bubble-' + sender; div.className = 'chat-bubble bubble-' + sender;
div.textContent = text; div.textContent = text;
@@ -718,7 +816,7 @@
async function loadConfig() { async function loadConfig() {
try { try {
const res = await fetch('/api/config'); const res = await apiFetch('/api/config');
const data = await res.json(); const data = await res.json();
const provider = document.getElementById('active_provider'); const provider = document.getElementById('active_provider');
const key = document.getElementById('gemini_api_key'); const key = document.getElementById('gemini_api_key');
@@ -732,7 +830,7 @@
const key = document.getElementById('gemini_api_key').value.trim(); const key = document.getElementById('gemini_api_key').value.trim();
try { try {
const res = await fetch('/api/config', { const res = await apiFetch('/api/config', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ active_provider: provider, gemini_api_key: key }) body: JSON.stringify({ active_provider: provider, gemini_api_key: key })
@@ -748,8 +846,6 @@
} }
} }
window.saveConfiguration = saveConfig;
window.sendMessage = sendChat;
async function testLLMSpeed() { async function testLLMSpeed() {
const btn = document.getElementById('btn-test-llm'); const btn = document.getElementById('btn-test-llm');
const originalContent = btn.innerHTML; const originalContent = btn.innerHTML;
@@ -757,7 +853,7 @@
btn.disabled = true; btn.disabled = true;
try { try {
const res = await fetch('/api/test_llm'); const res = await apiFetch('/api/test_llm');
const data = await res.json(); const data = await res.json();
if(data.status === 'success') { if(data.status === 'success') {
showToast(`✅ LLM Online! Resposta em ${data.latency}s`); showToast(`✅ LLM Online! Resposta em ${data.latency}s`);
@@ -778,10 +874,28 @@
} }
} }
window.saveConfiguration = saveConfig;
window.sendMessage = sendChat;
window.executeAction = executeAction; window.executeAction = executeAction;
window.testLLMSpeed = testLLMSpeed; 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> </script>
</body> </body>
</html> </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."