security: remove hardcoded secrets, add health endpoint, add security headers
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
from datetime import timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
import asyncio
|
||||
import os
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from pydantic import BaseModel
|
||||
import config
|
||||
import auth
|
||||
@@ -26,10 +29,33 @@ class Verify2FARequest(BaseModel):
|
||||
secret: str
|
||||
code: str
|
||||
|
||||
async def send_failed_login_alert(ip_address: str, username: str, reason: str):
|
||||
print(f"Triggering Telegram alert for {username} from {ip_address}: {reason}", flush=True)
|
||||
if not config.TELEGRAM_BOT_TOKEN or not config.TELEGRAM_CHAT_ID:
|
||||
print("Skipping Telegram alert: missing config", flush=True)
|
||||
return
|
||||
msg = f"🚨 *Dashboard Security Alert* 🚨\nFailed login attempt detected.\n\n👤 User: `{username}`\n🌐 IP: `{ip_address}`\n❌ Reason: {reason}"
|
||||
try:
|
||||
if config.TELEGRAM_PROXY:
|
||||
os.environ["HTTP_PROXY"] = config.TELEGRAM_PROXY
|
||||
os.environ["HTTPS_PROXY"] = config.TELEGRAM_PROXY
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
res = await client.post(
|
||||
f"https://api.telegram.org/bot{config.TELEGRAM_BOT_TOKEN}/sendMessage",
|
||||
data={"chat_id": config.TELEGRAM_CHAT_ID, "text": msg, "parse_mode": "Markdown"},
|
||||
timeout=10.0
|
||||
)
|
||||
print(f"Telegram alert sent, status: {res.status_code}", flush=True)
|
||||
except Exception as e:
|
||||
print(f"Failed to send Telegram alert: {e}", flush=True)
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(creds: LoginRequest):
|
||||
async def login(creds: LoginRequest, request: Request):
|
||||
client_ip = request.client.host
|
||||
# Verify username/password
|
||||
if creds.username != config.ADMIN_USER or not auth.verify_password(creds.password, auth.load_password_hash()):
|
||||
asyncio.create_task(send_failed_login_alert(client_ip, creds.username, "Incorrect username or password"))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
@@ -40,13 +66,15 @@ async def login(creds: LoginRequest):
|
||||
totp_secret = auth.load_totp_secret()
|
||||
if totp_secret:
|
||||
if not creds.totp_code:
|
||||
asyncio.create_task(send_failed_login_alert(client_ip, creds.username, "Missing 2FA Code"))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="2FA code required",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
if not auth.verify_totp(creds.totp_code, totp_secret):
|
||||
raise HTTPException(
|
||||
asyncio.create_task(send_failed_login_alert(client_ip, creds.username, "Invalid 2FA Code"))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid 2FA code",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
|
||||
Reference in New Issue
Block a user