🔒 Implementação de segurança: Login Web fixo e proteção de API
This commit is contained in:
@@ -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
64
main.py
@@ -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)
|
||||
|
||||
@@ -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
15
update.sh
Executable 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."
|
||||
Reference in New Issue
Block a user