From 200b8104a6fddb368eadefc143849bef812833c0 Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Feb 2026 02:26:07 +0300 Subject: [PATCH] Add NPMplus proxy hosts controls --- handlers/system.py | 69 ++++++++++++++++++++++++++++++++++++++++++++- keyboards.py | 4 +-- services/npmplus.py | 42 +++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 3 deletions(-) diff --git a/handlers/system.py b/handlers/system.py index 341794f..e96dab1 100644 --- a/handlers/system.py +++ b/handlers/system.py @@ -10,7 +10,7 @@ from services.http_checks import get_url_checks, check_url from services.queue import enqueue from services.updates import list_updates, apply_updates from services.runner import run_cmd -from services.npmplus import fetch_certificates, format_certificates +from services.npmplus import fetch_certificates, format_certificates, list_proxy_hosts, set_proxy_host import state from state import UPDATES_CACHE, REBOOT_PENDING from services.metrics import summarize @@ -246,6 +246,49 @@ async def ssl_certs(msg: Message): asyncio.create_task(worker()) +@dp.message(F.text == "🧩 NPMplus") +async def npmplus_hosts(msg: Message): + if not is_admin_msg(msg): + return + + await msg.answer("⏳ Loading NPMplus hosts…", reply_markup=system_logs_kb) + + async def worker(): + try: + hosts = await asyncio.to_thread(list_proxy_hosts, cfg) + except Exception as e: + await msg.answer(f"⚠️ NPMplus error: {e}", reply_markup=system_logs_kb) + return + + if not hosts: + await msg.answer("🧩 NPMplus\n\n(no proxy hosts)", reply_markup=system_logs_kb) + return + + lines = ["🧩 NPMplus proxy hosts\n"] + rows = [] + for h in hosts[:10]: + hid = h.get("id") + domains = h.get("domain_names") or [] + name = ", ".join(domains) if isinstance(domains, list) else str(domains) + forward = f"{h.get('forward_host')}:{h.get('forward_port')}" + enabled = h.get("enabled", True) + icon = "🟒" if enabled else "πŸ”΄" + lines.append(f"{icon} {name} β†’ {forward}") + if hid is not None: + action = "disable" if enabled else "enable" + rows.append([ + InlineKeyboardButton( + text=f\"{('β›”' if enabled else 'βœ…')} {name[:12]}\", + callback_data=f\"npmplus:{action}:{hid}\" + ) + ]) + + kb = InlineKeyboardMarkup(inline_keyboard=rows) + await msg.answer("\n".join(lines), reply_markup=kb) + + asyncio.create_task(worker()) + + @dp.message(F.text == "πŸ“¦ Updates") async def updates_list(msg: Message): if not is_admin_msg(msg): @@ -381,6 +424,30 @@ async def reboot_cancel(cb: CallbackQuery): await cb.message.delete() +@dp.callback_query(F.data.startswith("npmplus:")) +async def npmplus_toggle(cb: CallbackQuery): + if cb.from_user.id != cfg["telegram"]["admin_id"]: + return + parts = cb.data.split(":") + if len(parts) != 3: + await cb.answer("Bad request") + return + action, raw_id = parts[1], parts[2] + try: + host_id = int(raw_id) + except ValueError: + await cb.answer("Bad id") + return + + enable = action == "enable" + await cb.answer("Working…") + ok, info = await asyncio.to_thread(set_proxy_host, cfg, host_id, enable) + if ok: + await cb.message.answer("βœ… Updated", reply_markup=system_logs_kb) + else: + await cb.message.answer(f"❌ NPMplus error: {info}", reply_markup=system_logs_kb) + + @dp.message(F.text, F.func(lambda msg: msg.from_user and msg.from_user.id in REBOOT_PENDING)) async def reboot_password(msg: Message): if not is_admin_msg(msg): diff --git a/keyboards.py b/keyboards.py index 26228a8..9071f51 100644 --- a/keyboards.py +++ b/keyboards.py @@ -83,8 +83,8 @@ system_logs_kb = ReplyKeyboardMarkup( keyboard=[ [KeyboardButton(text="🧾 Audit"), KeyboardButton(text="πŸ“£ Incidents")], [KeyboardButton(text="🧰 Processes"), KeyboardButton(text="πŸ”’ SSL")], - [KeyboardButton(text="🌐 URLs"), KeyboardButton(text="πŸ”‘ SSH log")], - [KeyboardButton(text="🌍 External")], + [KeyboardButton(text="πŸ”‘ SSH log"), KeyboardButton(text="🧩 NPMplus")], + [KeyboardButton(text="🌍 External"), KeyboardButton(text="🌐 URLs")], [KeyboardButton(text="⬅️ System")], ], resize_keyboard=True, diff --git a/services/npmplus.py b/services/npmplus.py index 6418ed9..517b5ca 100644 --- a/services/npmplus.py +++ b/services/npmplus.py @@ -132,6 +132,48 @@ def fetch_certificates(cfg: dict[str, Any]) -> list[dict[str, Any]]: return data +def list_proxy_hosts(cfg: dict[str, Any]) -> list[dict[str, Any]]: + npm_cfg = cfg.get("npmplus", {}) + base_url = (npm_cfg.get("base_url") or "").rstrip("/") + verify_tls = npm_cfg.get("verify_tls", True) + if not base_url: + raise ValueError("NPMplus base_url not configured") + + token = _get_token(cfg) + url = f"{base_url}/nginx/proxy-hosts" + headers = { + "Authorization": f"Bearer {token}", + "User-Agent": "tg-admin-bot", + } + data = _request_json(url, headers, None, verify_tls) + if not isinstance(data, list): + raise RuntimeError("Unexpected API response") + return data + + +def set_proxy_host(cfg: dict[str, Any], host_id: int, enabled: bool) -> tuple[bool, str]: + npm_cfg = cfg.get("npmplus", {}) + base_url = (npm_cfg.get("base_url") or "").rstrip("/") + verify_tls = npm_cfg.get("verify_tls", True) + if not base_url: + return False, "NPMplus base_url not configured" + + token = _get_token(cfg) + action = "enable" if enabled else "disable" + url = f"{base_url}/nginx/proxy-hosts/{host_id}/{action}" + headers = { + "Authorization": f"Bearer {token}", + "User-Agent": "tg-admin-bot", + } + try: + payload = _request_json(url, headers, None, verify_tls) + except Exception as e: + return False, str(e) + if payload is True or (isinstance(payload, dict) and payload.get("success", True)): + return True, "OK" + return False, "API returned error" + + def format_certificates(certs: list[dict[str, Any]]) -> str: if not certs: return "πŸ”’ SSL certificates\n\nℹ️ No certificates found"