diff --git a/.gitignore b/.gitignore index 829a5ac..a370f08 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ node_modules/ dist/ .env *.tar.gz +dashboard/nas-dashboard.tar diff --git a/dashboard/backend/config.py b/dashboard/backend/config.py index 82f629b..6f1b0b9 100644 --- a/dashboard/backend/config.py +++ b/dashboard/backend/config.py @@ -7,13 +7,12 @@ DOCKER_HOST = os.environ.get("DOCKER_HOST", "tcp://docker-socket-proxy:2375") VOLUME_ROOT = os.environ.get("VOLUME_ROOT", "/volume1") # Auth -SECRET_KEY = os.environ.get("SECRET_KEY", "c1a776b228421830e7c7df0887adc75f74bac65aaa1fc364be96861b1f8c8701") +SECRET_KEY = os.environ.get("SECRET_KEY", "") ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days ADMIN_USER = os.environ.get("ADMIN_USER", "admin") -# Default hash for "admin" using pbkdf2_sha256 scheme to avoid bcrypt compat issues -ADMIN_PASSWORD_HASH = os.environ.get("ADMIN_PASSWORD_HASH", "$pbkdf2-sha256$29000$25tTKoUQIgQghLC2ds4ZYw$H/fCb3H5wmOkwj6l6KrVlmns0TWfzKPOrG3sgVDR.eg") -TOTP_SECRET = os.environ.get("TOTP_SECRET", "JBSWY3DPEHPK3PXP") # Base32 encoded secretans 2FA disabled by default +ADMIN_PASSWORD_HASH = os.environ.get("ADMIN_PASSWORD_HASH", "") +TOTP_SECRET = os.environ.get("TOTP_SECRET", "") # SSH terminal SSH_HOST = os.environ.get("SSH_HOST", "host.docker.internal") @@ -27,3 +26,4 @@ VPS_SSH_KEY_PATH = os.environ.get("VPS_SSH_KEY_PATH", "/app/ssh/vps_id_ed25519") # Telegram notifications TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "") TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "") +TELEGRAM_PROXY = os.environ.get("TELEGRAM_PROXY", "") diff --git a/dashboard/backend/main.py b/dashboard/backend/main.py index 0297207..a24cbd8 100644 --- a/dashboard/backend/main.py +++ b/dashboard/backend/main.py @@ -43,6 +43,11 @@ async def monitor_containers(): @app.on_event("startup") async def startup_event(): asyncio.create_task(monitor_containers()) + +@app.get("/api/health") +async def health(): + return {"status": "ok"} + # Public auth endpoints app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) diff --git a/dashboard/backend/routers/auth.py b/dashboard/backend/routers/auth.py index 6d448f0..c467a5f 100644 --- a/dashboard/backend/routers/auth.py +++ b/dashboard/backend/routers/auth.py @@ -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"}, diff --git a/dashboard/docker-compose.yml b/dashboard/docker-compose.yml index 94e25b7..9b56884 100644 --- a/dashboard/docker-compose.yml +++ b/dashboard/docker-compose.yml @@ -34,6 +34,8 @@ services: - SSH_USER=zjgump - VPS_SSH_HOST=158.101.140.85 - VPS_SSH_USER=jimmyg + - SECRET_KEY=${SECRET_KEY} + - ADMIN_PASSWORD_HASH=${ADMIN_PASSWORD_HASH} - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-} - TELEGRAM_PROXY=${TELEGRAM_PROXY:-} @@ -42,7 +44,7 @@ services: - /volume1/docker/nas-dashboard/ssh/dashboard_terminal:/app/ssh/id_ed25519:ro - /volume1/docker/nas-dashboard/ssh/vps_terminal:/app/ssh/vps_id_ed25519:ro healthcheck: - test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:4000/api/system/stats')"] + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:4000/api/health')"] interval: 30s timeout: 5s retries: 3