From ad0e80b15ca64a423aaae08194e21057d61707c9 Mon Sep 17 00:00:00 2001 From: nickpons666 Date: Fri, 6 Mar 2026 21:08:37 -0600 Subject: [PATCH] =?UTF-8?q?feat(panel):=20gesti=C3=B3n=20de=20m=C3=BAltipl?= =?UTF-8?q?es=20administradores=20en=20MySQL=20y=20simplificaci=C3=B3n=20d?= =?UTF-8?q?e=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- botdiscord/database.py | 105 ++++++++++++++++++++++++ panel/main.py | 88 +++++++++++++++++++- panel/templates/admins.html | 145 +++++++++++++++++++++++++++++++++ panel/templates/dashboard.html | 24 +++--- requirements.txt | 2 + 5 files changed, 349 insertions(+), 15 deletions(-) create mode 100644 panel/templates/admins.html diff --git a/botdiscord/database.py b/botdiscord/database.py index f0cf4b1..a9f95ab 100644 --- a/botdiscord/database.py +++ b/botdiscord/database.py @@ -117,6 +117,13 @@ def init_db(): created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY idx_ui_lang (original_text(255), target_lang)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''') + # Tabla para administradores del panel web + cursor.execute('''CREATE TABLE IF NOT EXISTS admins + (id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4''') + conn.commit() cursor.close() else: @@ -148,6 +155,22 @@ def init_db(): translated_text TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(message_id, target_lang))''') + + # SQLite equivalent for UI translations + c.execute('''CREATE TABLE IF NOT EXISTS ui_translations + (id INTEGER PRIMARY KEY AUTOINCREMENT, + original_text TEXT NOT NULL, + target_lang TEXT NOT NULL, + translated_text TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(original_text, target_lang))''') + + # SQLite equivalent for admins + c.execute('''CREATE TABLE IF NOT EXISTS admins + (id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''') conn.commit() conn.close() @@ -420,3 +443,85 @@ def save_ui_translation(text: str, target_lang: str, translated_text: str): c.execute("INSERT OR REPLACE INTO ui_translations (original_text, target_lang, translated_text) VALUES (?, ?, ?)", (text, target_lang, translated_text)) conn.commit() conn.close() + +# Funciones para administradores +def get_admins(): + db_type = get_db_type() + if db_type == "mysql": + conn = get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT id, username, created_at FROM admins ORDER BY username") + rows = cursor.fetchall() + cursor.close() + return rows + else: + conn = get_connection() + c = conn.cursor() + c.execute("SELECT id, username, created_at FROM admins ORDER BY username") + rows = [{"id": r[0], "username": r[1], "created_at": r[2]} for r in c.fetchall()] + conn.close() + return rows + +def get_admin_by_username(username: str): + db_type = get_db_type() + if db_type == "mysql": + conn = get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT * FROM admins WHERE username = %s", (username,)) + row = cursor.fetchone() + cursor.close() + return row + else: + conn = get_connection() + c = conn.cursor() + c.execute("SELECT id, username, password_hash, created_at FROM admins WHERE username = ?", (username,)) + row = c.fetchone() + conn.close() + if row: + return {"id": row[0], "username": row[1], "password_hash": row[2], "created_at": row[3]} + return None + +def add_admin(username: str, password_hash: str): + db_type = get_db_type() + if db_type == "mysql": + conn = get_connection() + cursor = conn.cursor() + cursor.execute("INSERT INTO admins (username, password_hash) VALUES (%s, %s)", (username, password_hash)) + conn.commit() + cursor.close() + else: + conn = get_connection() + c = conn.cursor() + c.execute("INSERT INTO admins (username, password_hash) VALUES (?, ?)", (username, password_hash)) + conn.commit() + conn.close() + +def delete_admin(admin_id: int): + db_type = get_db_type() + if db_type == "mysql": + conn = get_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM admins WHERE id = %s", (admin_id,)) + conn.commit() + cursor.close() + else: + conn = get_connection() + c = conn.cursor() + c.execute("DELETE FROM admins WHERE id = ?", (admin_id,)) + conn.commit() + conn.close() + +def update_admin_password(admin_id: int, password_hash: str): + db_type = get_db_type() + if db_type == "mysql": + conn = get_connection() + cursor = conn.cursor() + cursor.execute("UPDATE admins SET password_hash = %s WHERE id = %s", (password_hash, admin_id)) + conn.commit() + cursor.close() + else: + conn = get_connection() + c = conn.cursor() + c.execute("UPDATE admins SET password_hash = ? WHERE id = ?", (password_hash, admin_id)) + conn.commit() + conn.close() diff --git a/panel/main.py b/panel/main.py index 8bd6b90..c3c5b11 100644 --- a/panel/main.py +++ b/panel/main.py @@ -9,8 +9,12 @@ from fastapi import Request from fastapi.responses import RedirectResponse from pydantic import BaseModel +from passlib.hash import pbkdf2_sha256 as hasher from botdiscord.config import load_config, get_web_config, get_libretranslate_url, get_db_type -from botdiscord.database import get_ui_translation, save_ui_translation +from botdiscord.database import ( + get_ui_translation, save_ui_translation, + get_admins, get_admin_by_username, add_admin, delete_admin +) from botdiscord.translate import translate_text app = FastAPI(title="Panel de Configuración - Bots de Traducción") @@ -74,9 +78,21 @@ def get_config(): } def verify_admin(username: str, password: str) -> bool: + # 1. Primero intentamos con el SuperAdmin del .env (backup) web_config = get_web_config() - return (username == web_config.get("admin_username", "") and - password == web_config.get("admin_password", "")) + if username == web_config.get("admin_username", "") and \ + password == web_config.get("admin_password", ""): + return True + + # 2. Si no es el SuperAdmin, buscamos en la base de datos MySQL + try: + admin = get_admin_by_username(username) + if admin and hasher.verify(password, admin['password_hash']): + return True + except Exception as e: + print(f"Error verifying admin: {e}") + + return False @app.get("/") async def root(request: Request): @@ -217,6 +233,72 @@ async def logout(): response.delete_cookie("auth") return response +@app.get("/admins") +async def admins_page(request: Request): + if request.cookies.get("auth") != "ok": + return RedirectResponse(url="/login") + + admins = get_admins() + return templates.TemplateResponse("admins.html", { + "request": request, + "admins": admins + }) + +@app.post("/admins/add") +async def add_admin_post(request: Request): + if request.cookies.get("auth") != "ok": + raise HTTPException(status_code=401) + + form = await request.form() + username = form.get("username", "") + password = form.get("password", "") + + if not username or not password: + return RedirectResponse(url="/admins?error=missing_fields", status_code=status.HTTP_303_SEE_OTHER) + + try: + password_hash = hasher.hash(password) + add_admin(username, password_hash) + return RedirectResponse(url="/admins?success=1", status_code=status.HTTP_303_SEE_OTHER) + except Exception as e: + print(f"CRITICAL ERROR adding admin: {e}") + # Redirigimos con error a la misma página + return RedirectResponse(url="/admins?error=" + str(e), status_code=status.HTTP_303_SEE_OTHER) + +@app.post("/admins/delete") +async def delete_admin_post(request: Request): + if request.cookies.get("auth") != "ok": + raise HTTPException(status_code=401) + + form = await request.form() + admin_id = form.get("admin_id") + if admin_id: + try: + delete_admin(int(admin_id)) + except Exception as e: + print(f"Error deleting admin: {e}") + + return RedirectResponse(url="/admins", status_code=status.HTTP_303_SEE_OTHER) + +@app.post("/admins/update") +async def update_admin_post(request: Request): + if request.cookies.get("auth") != "ok": + raise HTTPException(status_code=401) + + form = await request.form() + admin_id = form.get("admin_id") + new_password = form.get("new_password") + + if admin_id and new_password: + from botdiscord.database import update_admin_password + password_hash = hasher.hash(new_password) + try: + update_admin_password(int(admin_id), password_hash) + except Exception as e: + print(f"Error updating admin password: {e}") + + return RedirectResponse(url="/admins?success=1", status_code=status.HTTP_303_SEE_OTHER) + if __name__ == "__main__": import uvicorn web_config = get_web_config() diff --git a/panel/templates/admins.html b/panel/templates/admins.html new file mode 100644 index 0000000..025ea47 --- /dev/null +++ b/panel/templates/admins.html @@ -0,0 +1,145 @@ +{% set lang = request.cookies.get('panel_lang', 'es') %} + + + + + + {{ "Administradores - Bots de Traducción" | translate(lang) }} + + + + + + +
+

{{ "Gestión de Administradores" | translate(lang) }}

+ +
+
+
+
+
{{ "Añadir Administrador" | translate(lang) }}
+
+
+
+
+ + +
+
+ + +
+ +
+
+
+
+ +
+
+
+
{{ "Lista de Administradores" | translate(lang) }}
+
+
+ + + + + + + + + + + {% for admin in admins %} + + + + + + + {% endfor %} + +
{{ "ID" | translate(lang) }}{{ "Usuario" | translate(lang) }}{{ "Fecha Creación" | translate(lang) }}{{ "Acciones" | translate(lang) }}
{{ admin.id }}{{ admin.username }}{{ admin.created_at }} + +
+ + +
+
+
+
+
+
+
+ + + + + + + + diff --git a/panel/templates/dashboard.html b/panel/templates/dashboard.html index 445e8a9..3de00b6 100644 --- a/panel/templates/dashboard.html +++ b/panel/templates/dashboard.html @@ -38,18 +38,8 @@
-
Discord
-

{{ "Bot de traducción para servidores de Discord" | translate(lang) }}

- {{ "Configurar" | translate(lang) }} -
-
-
- -
-
-
-
Telegram
-

{{ "Bot de traducción para grupos de Telegram" | translate(lang) }}

+
{{ "Configuración" | translate(lang) }}
+

{{ "Configurar tokens de bots y parámetros del sistema" | translate(lang) }}

{{ "Configurar" | translate(lang) }}
@@ -64,6 +54,16 @@
+ +
+
+
+
{{ "Administradores" | translate(lang) }}
+

{{ "Gestionar usuarios del panel web" | translate(lang) }}

+ {{ "Gestionar" | translate(lang) }} +
+
+
diff --git a/requirements.txt b/requirements.txt index fe1beec..9086b5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,5 @@ python-dotenv>=1.0.0 python-multipart>=0.0.9 mysql-connector-python>=8.0.0 nest-asyncio +bcrypt +passlib