diff --git a/src/app.py b/src/app.py index 2b2df92..87904b8 100644 --- a/src/app.py +++ b/src/app.py @@ -72,11 +72,15 @@ async def lifespan(app: FastAPI): tasks_master = get_tasksmaster() tasks_master.run_scheduled_tasks() + password_line = "" + if config.dashboard_password_generated: + password_line = f"\n\nDASHBOARD PASSWORD (auto-generated)\n{config.dashboard_password}" + banner = f""" ============================================================ DASHBOARD AVAILABLE AT -{config.dashboard_secret_path} +{config.dashboard_secret_path}{password_line} ============================================================ """ app_logger.info(banner) diff --git a/src/config.py b/src/config.py index 0d6d648..f43b390 100644 --- a/src/config.py +++ b/src/config.py @@ -29,6 +29,7 @@ class Config: canary_token_tries: int = 10 dashboard_secret_path: str = None dashboard_password: Optional[str] = None + dashboard_password_generated: bool = False probability_error_codes: int = 0 # Percentage (0-100) # Crawl limiting settings - for legitimate vs malicious crawlers @@ -179,8 +180,10 @@ class Config: # Handle dashboard_password - auto-generate if null/not set dashboard_password = dashboard.get("password") + dashboard_password_generated = False if dashboard_password is None: dashboard_password = os.urandom(25).hex() + dashboard_password_generated = True return cls( port=server.get("port", 5000), @@ -203,6 +206,7 @@ class Config: canary_token_tries=canary.get("token_tries", 10), dashboard_secret_path=dashboard_path, dashboard_password=dashboard_password, + dashboard_password_generated=dashboard_password_generated, probability_error_codes=behavior.get("probability_error_codes", 0), exports_path=exports.get("path", "exports"), backups_path=backups.get("path", "backups"), diff --git a/src/routes/api.py b/src/routes/api.py index d94b3b6..9ef8f0f 100644 --- a/src/routes/api.py +++ b/src/routes/api.py @@ -7,13 +7,19 @@ All endpoints are prefixed with the secret dashboard path. """ import os +import secrets +import hmac -from fastapi import APIRouter, Request, Response, Query +from fastapi import APIRouter, Request, Response, Query, Cookie from fastapi.responses import JSONResponse, PlainTextResponse +from pydantic import BaseModel from dependencies import get_db from logger import get_app_logger +# Server-side session token store (valid tokens for authenticated sessions) +_auth_tokens: set = set() + router = APIRouter() @@ -26,6 +32,54 @@ def _no_cache_headers() -> dict: } +class AuthRequest(BaseModel): + password: str + + +def verify_auth(request: Request) -> bool: + """Check if the request has a valid auth session cookie.""" + token = request.cookies.get("krawl_auth") + return token is not None and token in _auth_tokens + + +@router.post("/api/auth") +async def authenticate(request: Request, body: AuthRequest): + config = request.app.state.config + if hmac.compare_digest(body.password, config.dashboard_password): + token = secrets.token_hex(32) + _auth_tokens.add(token) + response = JSONResponse(content={"authenticated": True}) + response.set_cookie( + key="krawl_auth", + value=token, + httponly=True, + samesite="strict", + ) + return response + return JSONResponse( + content={"authenticated": False, "error": "Invalid password"}, + status_code=401, + ) + + +@router.post("/api/auth/logout") +async def logout(request: Request): + token = request.cookies.get("krawl_auth") + if token and token in _auth_tokens: + _auth_tokens.discard(token) + response = JSONResponse(content={"authenticated": False}) + response.delete_cookie(key="krawl_auth") + return response + + +@router.get("/api/auth/check") +async def auth_check(request: Request): + """Check if the current session is authenticated.""" + if verify_auth(request): + return JSONResponse(content={"authenticated": True}) + return JSONResponse(content={"authenticated": False}, status_code=401) + + @router.get("/api/all-ip-stats") async def all_ip_stats(request: Request): db = get_db() diff --git a/src/routes/htmx.py b/src/routes/htmx.py index 549f044..a793c87 100644 --- a/src/routes/htmx.py +++ b/src/routes/htmx.py @@ -6,8 +6,10 @@ Server-rendered HTML partials for table pagination, sorting, IP details, and sea """ from fastapi import APIRouter, Request, Response, Query +from fastapi.responses import HTMLResponse from dependencies import get_db, get_templates +from routes.api import verify_auth router = APIRouter() @@ -408,3 +410,27 @@ async def htmx_search( "pagination": result["pagination"], }, ) + + +# ── Protected Admin Panel ──────────────────────────────────────────── + + +@router.get("/htmx/admin") +async def htmx_admin(request: Request): + if not verify_auth(request): + return HTMLResponse( + '
'
+ 'This is a protected panel. More features coming soon.
+