From dfe00a280c37686df9cc0ad5a1bc9e0d32fcc125 Mon Sep 17 00:00:00 2001 From: g0ldyy <153996346+g0ldyy@users.noreply.github.com> Date: Sat, 23 Aug 2025 23:47:47 +0200 Subject: [PATCH] feat: implement new admin dashboard --- .env-sample | 2 +- comet/api/core.py | 224 ++- comet/main.py | 2 +- comet/templates/admin_dashboard.html | 725 +++++++++ comet/templates/admin_login.html | 333 +++++ comet/templates/index.html | 2056 ++++++++++++++------------ comet/utils/log_levels.py | 49 + comet/utils/logger.py | 22 +- comet/utils/models.py | 2 +- 9 files changed, 2449 insertions(+), 966 deletions(-) create mode 100644 comet/templates/admin_dashboard.html create mode 100644 comet/templates/admin_login.html create mode 100644 comet/utils/log_levels.py diff --git a/.env-sample b/.env-sample index 7aaa441..f0b38bd 100644 --- a/.env-sample +++ b/.env-sample @@ -24,7 +24,7 @@ USE_GUNICORN=True # Will use uvicorn if False or if on Windows # ============================== # # Dashboard Settings # # ============================== # -DASHBOARD_ADMIN_PASSWORD=CHANGE_ME # The password to access the dashboard +ADMIN_DASHBOARD_PASSWORD=CHANGE_ME # The password to access the dashboard # ============================== # # Database Configuration # diff --git a/comet/api/core.py b/comet/api/core.py index dd91421..efc05db 100644 --- a/comet/api/core.py +++ b/comet/api/core.py @@ -2,20 +2,144 @@ import random import string import secrets import orjson +import uuid +import time +import re -from fastapi import APIRouter, Request, Depends, HTTPException -from fastapi.responses import RedirectResponse, Response +from loguru import logger as loguru_logger + +from fastapi import APIRouter, Request, HTTPException, Form, Cookie +from fastapi.responses import RedirectResponse, Response, JSONResponse from fastapi.templating import Jinja2Templates -from fastapi.security import HTTPBasic, HTTPBasicCredentials +from fastapi.security import HTTPBasic from comet.utils.models import settings, web_config, database from comet.utils.general import config_check +from comet.utils.log_levels import get_level_info from comet.debrid.manager import get_debrid_extension templates = Jinja2Templates("comet/templates") main = APIRouter() security = HTTPBasic() +admin_sessions = {} + + +class LogCapture: + def __init__(self): + self.logs = [] + self.max_logs = 1000 + + def add_log(self, record): + # Format the log record similar to loguru format + timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created)) + level_name = record.levelname + + # Handle special loguru levels + if hasattr(record, "extra") and "level_name" in record.extra: + level_name = record.extra["level_name"] + + level_info = get_level_info(level_name) + + log_entry = { + "timestamp": timestamp, + "level": level_name, + "icon": level_info["icon"], + "color": level_info["color"], + "module": getattr(record, "module", "unknown"), + "function": getattr(record, "funcName", "unknown"), + "message": record.getMessage(), + "created": record.created, + } + + self.logs.append(log_entry) + if len(self.logs) > self.max_logs: + self.logs.pop(0) + + def get_logs(self): + return self.logs + + +# Global log capture instance +log_capture = LogCapture() + + +# Loguru handler to capture logs +class LoguruHandler: + def __init__(self, log_capture): + self.log_capture = log_capture + + def write(self, message): + if message.strip(): + # Try to extract timestamp, level, module, function, and message + # This is a simplified parser for loguru format + pattern = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \| ([🌠👾👻🎬🔒📰🕸️⚠️❌💀]?) ?(\w+) \| (\w+)\.(\w+) - (.+)" + match = re.match(pattern, message.strip()) + + if match: + timestamp_str, icon, level, module, function, msg = match.groups() + + level_info = get_level_info(level) + + log_entry = { + "timestamp": timestamp_str, + "level": level, + "icon": icon or level_info["icon"], + "color": level_info["color"], + "module": module, + "function": function, + "message": msg, + "created": time.time(), + } + + self.log_capture.add_log_entry(log_entry) + + +# Add method to log capture to handle parsed entries +def add_log_entry_to_capture(self, log_entry): + self.logs.append(log_entry) + if len(self.logs) > self.max_logs: + self.logs.pop(0) + + +# Monkey patch the method +log_capture.add_log_entry = add_log_entry_to_capture.__get__(log_capture, LogCapture) + +# Set up loguru handler +loguru_handler = LoguruHandler(log_capture) + +# Add our handler to loguru +loguru_logger.add( + loguru_handler.write, + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level.icon} {level} | {module}.{function} - {message}", +) + + +def create_admin_session() -> str: + session_id = str(uuid.uuid4()) + admin_sessions[session_id] = {"created_at": time.time()} + return session_id + + +def verify_admin_session(admin_session: str = Cookie(None)): + if not admin_session or admin_session not in admin_sessions: + return False + + session_data = admin_sessions[admin_session] + + # Check if session is older than 24 hours + if time.time() - session_data["created_at"] > 86400: + del admin_sessions[admin_session] + return False + + return True + + +def require_admin_auth(admin_session: str = Cookie(None)): + if not verify_admin_session(admin_session): + raise HTTPException(status_code=401, detail="Authentication required") + @main.get("/") async def root(): @@ -80,29 +204,87 @@ async def manifest(request: Request, b64config: str = None): return base_manifest -class CustomORJSONResponse(Response): - media_type = "application/json" - - def render(self, content): - return orjson.dumps(content, option=orjson.OPT_INDENT_2) +@main.get("/admin") +async def admin_root(request: Request, admin_session: str = Cookie(None)): + if verify_admin_session(admin_session): + return RedirectResponse("/admin/dashboard") + return templates.TemplateResponse("admin_login.html", {"request": request}) -def verify_dashboard_auth(credentials: HTTPBasicCredentials = Depends(security)): - is_correct = secrets.compare_digest( - credentials.password, settings.DASHBOARD_ADMIN_PASSWORD - ) +@main.post("/admin/login") +async def admin_login(request: Request, password: str = Form(...)): + is_correct = secrets.compare_digest(password, settings.ADMIN_DASHBOARD_PASSWORD) if not is_correct: - raise HTTPException( - status_code=401, - detail="Incorrect password", - headers={"WWW-Authenticate": "Basic"}, + return templates.TemplateResponse( + "admin_login.html", {"request": request, "error": "Invalid password"} ) - return True + session_id = create_admin_session() + response = RedirectResponse("/admin/dashboard", status_code=303) + response.set_cookie( + key="admin_session", + value=session_id, + httponly=True, + secure=False, + samesite="lax", + max_age=86400, + ) + return response -@main.get("/dashboard", response_class=CustomORJSONResponse) -async def dashboard(authenticated: bool = Depends(verify_dashboard_auth)): - rows = await database.fetch_all("SELECT * FROM active_connections") - return rows +@main.get("/admin/dashboard") +async def admin_dashboard(request: Request, admin_session: str = Cookie(None)): + try: + require_admin_auth(admin_session) + return templates.TemplateResponse("admin_dashboard.html", {"request": request}) + except HTTPException: + return RedirectResponse("/admin", status_code=303) + + +@main.post("/admin/logout") +async def admin_logout(admin_session: str = Cookie(None)): + if admin_session and admin_session in admin_sessions: + del admin_sessions[admin_session] + + response = RedirectResponse("/admin", status_code=303) + response.delete_cookie("admin_session") + return response + + +@main.get("/admin/api/connections") +async def admin_api_connections(admin_session: str = Cookie(None)): + require_admin_auth(admin_session) + rows = await database.fetch_all( + "SELECT id, ip, content, timestamp FROM active_connections ORDER BY timestamp DESC" + ) + + connections = [] + for row in rows: + connections.append( + { + "id": row["id"], + "ip": row["ip"], + "content": row["content"], + "timestamp": row["timestamp"], + "duration": time.time() - row["timestamp"], + "formatted_time": time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(row["timestamp"]) + ), + } + ) + + return JSONResponse({"connections": connections}) + + +@main.get("/admin/api/logs") +async def admin_api_logs(admin_session: str = Cookie(None), since: float = 0): + require_admin_auth(admin_session) + + # Get logs since the specified timestamp + all_logs = log_capture.get_logs() + new_logs = [log for log in all_logs if log["created"] > since] + + return JSONResponse( + {"logs": new_logs, "total_logs": len(all_logs), "new_logs": len(new_logs)} + ) diff --git a/comet/main.py b/comet/main.py index c32fc7e..f5028e4 100644 --- a/comet/main.py +++ b/comet/main.py @@ -126,7 +126,7 @@ def start_log(): ) logger.log( "COMET", - f"Dashboard Admin Password: {settings.DASHBOARD_ADMIN_PASSWORD} - http://{settings.FASTAPI_HOST}:{settings.FASTAPI_PORT}/dashboard", + f"Admin Dashboard Password: {settings.ADMIN_DASHBOARD_PASSWORD} - http://{settings.FASTAPI_HOST}:{settings.FASTAPI_PORT}/admin", ) logger.log( "COMET", diff --git a/comet/templates/admin_dashboard.html b/comet/templates/admin_dashboard.html new file mode 100644 index 0000000..97fae76 --- /dev/null +++ b/comet/templates/admin_dashboard.html @@ -0,0 +1,725 @@ + + + + + + + + + + Comet - Dashboard + + + + + + + + + +
+
+
+

+ + Comet +

+
Dashboard
+
+
+ +
+ + + Logout + +
+
+
+ +
+ + + +  Active Connections + + + +  System Logs + + + +
+
+
+
0
+
Active Connections
+
+
+
0
+
Unique IPs
+
+
+
0s
+
Avg Duration
+
+
+ +
+
+ +

No active connections

+
+
+
+
+ + +
+
+
+ + + Clear Display + + + + Download Logs + +
+
+ Hide API Logs + Auto Refresh + + 1s + 2s + 5s + 10s + +
+
+ +
+
+ +

Loading system logs...

+
+
+
+
+
+
+
+ + + + diff --git a/comet/templates/admin_login.html b/comet/templates/admin_login.html new file mode 100644 index 0000000..6848acb --- /dev/null +++ b/comet/templates/admin_login.html @@ -0,0 +1,333 @@ + + + + + + + + + + Comet - Login + + + + + + + + + +
+ + + +
+

+ + Comet +

+
+ +
+ {% if error %} +
+ + + {{ error }} + +
+ {% endif %} + +
+
+ +
+ +
+ +
+
+
+ + + + diff --git a/comet/templates/index.html b/comet/templates/index.html index 36572f1..4e0a92c 100644 --- a/comet/templates/index.html +++ b/comet/templates/index.html @@ -1,947 +1,1135 @@ - - - - - - - - + + + + + + + + - Comet - Stremio's fastest torrent/debrid search add-on. - + Comet - Stremio's fastest torrent/debrid search add-on. + - - + + - + + + +
+ + +
+

+ + Comet - GitHub +

+ {{CUSTOM_HEADER_HTML|safe}} +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ + 4K - 2160p + Full HD - 1080p + HD - 720p + SD - 480p + LD - 360p + Unknown + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + Torrent + TorBox + EasyDebrid + Real-Debrid + Debrid-Link + All-Debrid + Premiumize + Offcloud + PikPak + +
+ +
+ + +
+ + +
+ + Show Cached Only + Allow English in Languages + Remove Unknown Languages + Remove Trash + + +
+
+ + + +
+ Install + + + Attempting to add the addon to Stremio... + + + Copy Link + + + The Stremio addon link has been automatically copied. + + + -
-

- - Comet - GitHub -

- {{CUSTOM_HEADER_HTML|safe}} -
- -
-
- -
- -
- -
- -
- -
- -
- - 4K - 2160p - Full HD - 1080p - HD - 720p - SD - 480p - LD - 360p - Unknown - -
- -
- -
- -
- -
- -
- -
- -
- - Torrent - TorBox - EasyDebrid - Real-Debrid - Debrid-Link - All-Debrid - Premiumize - Offcloud - PikPak - -
- -
- - -
- - -
- - Show Cached Only - Allow English in Languages - Remove Unknown Languages - Remove Trash - - -
-
- - - -
- Install - - - Attempting to add the addon to Stremio... - - - Copy Link - - - The Stremio addon link has been automatically copied. - - - -
-
- +
+
+ diff --git a/comet/utils/log_levels.py b/comet/utils/log_levels.py new file mode 100644 index 0000000..df717d0 --- /dev/null +++ b/comet/utils/log_levels.py @@ -0,0 +1,49 @@ +STANDARD_LOG_LEVELS = { + "DEBUG": {"color": "#DC5F00", "icon": "🕸️", "loguru_color": ""}, + "INFO": {"color": "#FC5F39", "icon": "📰", "loguru_color": ""}, + "WARNING": {"color": "#DC5F00", "icon": "⚠️", "loguru_color": ""}, + "ERROR": {"color": "#ff0000", "icon": "❌", "loguru_color": ""}, + "CRITICAL": {"color": "#ff0000", "icon": "💀", "loguru_color": ""}, +} + +CUSTOM_LOG_LEVELS = { + "COMET": { + "color": "#7871d6", + "icon": "🌠", + "loguru_color": "", + "no": 50, + }, + "API": {"color": "#006989", "icon": "👾", "loguru_color": "", "no": 45}, + "SCRAPER": { + "color": "#d6bb71", + "icon": "👻", + "loguru_color": "", + "no": 40, + }, + "STREAM": { + "color": "#d171d6", + "icon": "🎬", + "loguru_color": "", + "no": 35, + }, + "LOCK": { + "color": "#71d6d6", + "icon": "🔒", + "loguru_color": "", + "no": 30, + }, +} + +ALL_LOG_LEVELS = {**STANDARD_LOG_LEVELS, **CUSTOM_LOG_LEVELS} + + +def get_level_info(level_name: str) -> dict: + return ALL_LOG_LEVELS.get(level_name, {"color": "#ffffff", "icon": "📝"}) + + +def get_level_color(level_name: str) -> str: + return get_level_info(level_name)["color"] + + +def get_level_icon(level_name: str) -> str: + return get_level_info(level_name)["icon"] diff --git a/comet/utils/logger.py b/comet/utils/logger.py index f495d1c..9b714ff 100644 --- a/comet/utils/logger.py +++ b/comet/utils/logger.py @@ -2,6 +2,7 @@ import sys import logging from loguru import logger +from comet.utils.log_levels import CUSTOM_LOG_LEVELS, STANDARD_LOG_LEVELS logging.getLogger("demagnetize").setLevel( logging.CRITICAL @@ -9,15 +10,20 @@ logging.getLogger("demagnetize").setLevel( def setupLogger(level: str): - logger.level("COMET", no=50, icon="🌠", color="") - logger.level("API", no=45, icon="👾", color="") - logger.level("SCRAPER", no=40, icon="👻", color="") - logger.level("STREAM", no=35, icon="🎬", color="") - logger.level("LOCK", no=30, icon="🔒", color="") + # Configure custom log levels + for level_name, level_config in CUSTOM_LOG_LEVELS.items(): + logger.level( + level_name, + no=level_config["no"], + icon=level_config["icon"], + color=level_config["loguru_color"], + ) - logger.level("INFO", icon="📰", color="") - logger.level("DEBUG", icon="🕸️", color="") - logger.level("WARNING", icon="⚠️", color="") + # Configure standard log levels (override defaults) + for level_name, level_config in STANDARD_LOG_LEVELS.items(): + logger.level( + level_name, icon=level_config["icon"], color=level_config["loguru_color"] + ) log_format = ( "{time:YYYY-MM-DD} {time:HH:mm:ss} | " diff --git a/comet/utils/models.py b/comet/utils/models.py index 1645c3b..ec6944d 100644 --- a/comet/utils/models.py +++ b/comet/utils/models.py @@ -32,7 +32,7 @@ class AppSettings(BaseSettings): FASTAPI_PORT: Optional[int] = 8000 FASTAPI_WORKERS: Optional[int] = 1 USE_GUNICORN: Optional[bool] = True - DASHBOARD_ADMIN_PASSWORD: Optional[str] = "".join( + ADMIN_DASHBOARD_PASSWORD: Optional[str] = "".join( random.choices(string.ascii_letters + string.digits, k=16) ) DATABASE_TYPE: Optional[str] = "sqlite"