mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-06-15 04:02:46 +00:00
v2.5.0: remove local web admin token gate
This commit is contained in:
@@ -445,12 +445,12 @@ switch_language ru|en
|
|||||||
- frontend: `admin-web/static/`, vanilla JS/CSS, canvas-график без CDN;
|
- frontend: `admin-web/static/`, vanilla JS/CSS, canvas-график без CDN;
|
||||||
- systemd service: `gotelegram-admin`;
|
- systemd service: `gotelegram-admin`;
|
||||||
- bind: `127.0.0.1:1984`, доступ только через SSH tunnel;
|
- bind: `127.0.0.1:1984`, доступ только через SSH tunnel;
|
||||||
- token: `/opt/gotelegram-admin/token`, HTTP cookie `gtauth` выставляется при открытии `/?token=...`;
|
- токена нет: после SSH tunnel открывается `http://127.0.0.1:1984/`;
|
||||||
- API требует cookie/Bearer token, а write-запросы дополнительно требуют `X-GoTelegram-Admin: 1`.
|
- write-запросы дополнительно требуют `X-GoTelegram-Admin: 1`, фронтенд добавляет его автоматически.
|
||||||
|
|
||||||
Функции: overview, service status/restart, чтение/запись `[access.users]`, генерация proxy links, traffic history из `/opt/gotelegram/stats_history.csv`, current stats из `/run/gotelegram/stats_current.json`, список/создание backup, journal logs.
|
Функции: overview, service status/restart, чтение/запись `[access.users]`, генерация proxy links, traffic history из `/opt/gotelegram/stats_history.csv`, current stats из `/run/gotelegram/stats_current.json`, список/создание backup, journal logs.
|
||||||
|
|
||||||
`install_admin_web` вызывается при установке Telegram-бота. `auto_install_admin_web_if_possible` подхватывает админку после bootstrap/update, если Python уже установлен и файлы отличаются. Backup v1.3 сохраняет `admin_web/token`, `server.py` и `static/`, restore возвращает их и пробует перезапустить `gotelegram-admin`.
|
`install_admin_web` вызывается при установке Telegram-бота. `auto_install_admin_web_if_possible` подхватывает админку после bootstrap/update, если Python уже установлен и файлы отличаются. Backup v1.3 сохраняет `admin_web/server.py` и `admin_web/static/`, restore возвращает их, удаляет legacy `admin_web/token` и пробует перезапустить `gotelegram-admin`.
|
||||||
|
|
||||||
### 13.2 Upgrade migration (v2.5.0)
|
### 13.2 Upgrade migration (v2.5.0)
|
||||||
|
|
||||||
@@ -626,7 +626,7 @@ with socket.create_connection(("95.163.176.222", 443), timeout=5) as s:
|
|||||||
|
|
||||||
## 17. Changelog
|
## 17. Changelog
|
||||||
|
|
||||||
- **2.5.0 (2026-04-24)** — крупный maintenance pass в ветке `codex`: единая версия `2.5.0` в runtime и документации; удалён дефолтный PAT из `bootstrap.sh` (токен теперь только через `GOTELEGRAM_PAT`); `generate_telemt_toml` добавляет `[server.api]` на `127.0.0.1:9091` и metrics на `127.0.0.1:9090`, что нужно для управления пользователями и статистики; Telegram-бот получил меню `🔑 Keys` для `[access.users]` (добавить/удалить/показать ссылку/runtime info); добавлена локальная web-админка `gotelegram-admin` на `127.0.0.1:1984` с SSH-tunnel инструкцией в боте; исправлено чтение traffic CSV в боте (header больше не ломает parsing); бот сам делает `stats_collect` перед показом статистики; `iptables` добавлен в optional deps и stats collector пытается установить его; CLI-смена шаблона теперь обновляет `config.json.template_id`, чтобы бот не показывал первый установленный шаблон; backup/restore версии `1.3` сохраняет bot `.env`, bot lang files, web-admin token/static, custom templates, templates catalog, stats history и полноценную структуру Let's Encrypt (`live/archive/renewal`) для переезда на новый сервер; добавлен безопасный детект 3x-ui/Xray на 443 и генерируется `/opt/gotelegram/shared-443-3xui.md` с объяснением shared-443 ограничений.
|
- **2.5.0 (2026-04-24)** — крупный maintenance pass в ветке `codex`: единая версия `2.5.0` в runtime и документации; удалён дефолтный PAT из `bootstrap.sh` (токен теперь только через `GOTELEGRAM_PAT`); `generate_telemt_toml` добавляет `[server.api]` на `127.0.0.1:9091` и metrics на `127.0.0.1:9090`, что нужно для управления пользователями и статистики; Telegram-бот получил меню `🔑 Keys` для `[access.users]` (добавить/удалить/показать ссылку/runtime info); добавлена локальная web-админка `gotelegram-admin` на `127.0.0.1:1984` с SSH-tunnel инструкцией в боте без отдельного web-admin токена; исправлено чтение traffic CSV в боте (header больше не ломает parsing); бот сам делает `stats_collect` перед показом статистики; `iptables` добавлен в optional deps и stats collector пытается установить его; CLI-смена шаблона теперь обновляет `config.json.template_id`, чтобы бот не показывал первый установленный шаблон; backup/restore версии `1.3` сохраняет bot `.env`, bot lang files, web-admin server/static, custom templates, templates catalog, stats history и полноценную структуру Let's Encrypt (`live/archive/renewal`) для переезда на новый сервер; добавлен безопасный детект 3x-ui/Xray на 443 и генерируется `/opt/gotelegram/shared-443-3xui.md` с объяснением shared-443 ограничений.
|
||||||
- **2.4.6 (2026-04-10)** — universal `apt_lock_wait` helper: ожидание dpkg/apt lock при unattended-upgrades, исправляет установку nginx/certbot/python на свежих VPS.
|
- **2.4.6 (2026-04-10)** — universal `apt_lock_wait` helper: ожидание dpkg/apt lock при unattended-upgrades, исправляет установку nginx/certbot/python на свежих VPS.
|
||||||
- **2.4.3 (2026-04-10)** — iter3-фикс: `bot_action_dispatch` оборачивается во `flock -w 30` на `/var/lock/gotelegram-bot-action.lock`. Обнаружена гонка: параллельные `change-lite-domain` получали `"no secret in config"`, потому что один процесс читал `config.json`, пока другой делал `jq ... > tmp && mv`. `util-linux` (содержит `flock`) добавлен в `critical` deps, `check_deps_present` и маппинги `apt_pkg_for_cmd`/`dnf_pkg_for_cmd`.
|
- **2.4.3 (2026-04-10)** — iter3-фикс: `bot_action_dispatch` оборачивается во `flock -w 30` на `/var/lock/gotelegram-bot-action.lock`. Обнаружена гонка: параллельные `change-lite-domain` получали `"no secret in config"`, потому что один процесс читал `config.json`, пока другой делал `jq ... > tmp && mv`. `util-linux` (содержит `flock`) добавлен в `critical` deps, `check_deps_present` и маппинги `apt_pkg_for_cmd`/`dnf_pkg_for_cmd`.
|
||||||
- **2.4.2 (2026-04-10)** — реализация non-interactive `bot_action_*` в install.sh (change-template + change-lite-domain с JSON-ответом). bot.py подключает `run_bot_action()` и делает реальную работу вместо stub'ов. Критфиксы: (a) `safe_edit_message` принимает `disable_web_page_preview` (иначе TypeError в success-пути cb_pro_confirm); (b) чтение/запись `config['template_id']` вместо `config['template']` (save_gotelegram_config всегда писал `template_id`, бот смотрел не туда); (c) `bot_update_config_field` использует shell `date -Iseconds` вместо `jq now|todate` (jq 1.5 совместимость для Debian 10); (d) `asyncio.Lock _BOT_ACTION_LOCK` сериализует callback'и в процессе бота; (e) валидация `_TPL_ID_RE`/`_DOMAIN_RE` до subprocess. Полный аудит и автоустановка зависимостей: `ensure_deps` разделяет critical/optional, дедуплицирует пакеты, re-verify после install; `apt_pkg_for_cmd` и `dnf_pkg_for_cmd` мапят команды на пакеты (dig→dnsutils/bind-utils, xxd→xxd/vim-common, flock→util-linux). `check_deps_present` — быстрый чек без `apt-get update`.
|
- **2.4.2 (2026-04-10)** — реализация non-interactive `bot_action_*` в install.sh (change-template + change-lite-domain с JSON-ответом). bot.py подключает `run_bot_action()` и делает реальную работу вместо stub'ов. Критфиксы: (a) `safe_edit_message` принимает `disable_web_page_preview` (иначе TypeError в success-пути cb_pro_confirm); (b) чтение/запись `config['template_id']` вместо `config['template']` (save_gotelegram_config всегда писал `template_id`, бот смотрел не туда); (c) `bot_update_config_field` использует shell `date -Iseconds` вместо `jq now|todate` (jq 1.5 совместимость для Debian 10); (d) `asyncio.Lock _BOT_ACTION_LOCK` сериализует callback'и в процессе бота; (e) валидация `_TPL_ID_RE`/`_DOMAIN_RE` до subprocess. Полный аудит и автоустановка зависимостей: `ensure_deps` разделяет critical/optional, дедуплицирует пакеты, re-verify после install; `apt_pkg_for_cmd` и `dnf_pkg_for_cmd` мапят команды на пакеты (dig→dnsutils/bind-utils, xxd→xxd/vim-common, flock→util-linux). `check_deps_present` — быстрый чек без `apt-get update`.
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ CLI и бот переведены на русский и английский.
|
|||||||
- systemd service: `gotelegram-admin`;
|
- systemd service: `gotelegram-admin`;
|
||||||
- слушает только `127.0.0.1:1984`;
|
- слушает только `127.0.0.1:1984`;
|
||||||
- наружу не публикуется и рассчитана на доступ через SSH tunnel;
|
- наружу не публикуется и рассчитана на доступ через SSH tunnel;
|
||||||
- токен доступа хранится в `/opt/gotelegram-admin/token`;
|
- после туннеля открывается обычным URL `http://127.0.0.1:1984/`;
|
||||||
- Telegram-бот показывает инструкцию для Termius и обычную команду `ssh -L 1984:127.0.0.1:1984 root@SERVER`.
|
- Telegram-бот показывает инструкцию для Termius и обычную команду `ssh -L 1984:127.0.0.1:1984 root@SERVER`.
|
||||||
|
|
||||||
В админке есть dashboard, статус сервисов, управление `[access.users]`, генерация ссылок, график трафика, список бекапов и просмотр логов.
|
В админке есть dashboard, статус сервисов, управление `[access.users]`, генерация ссылок, график трафика, список бекапов и просмотр логов.
|
||||||
@@ -236,7 +236,7 @@ A: Сам MTProxy — да, это публичная технология из
|
|||||||
|
|
||||||
## Changelog (коротко)
|
## Changelog (коротко)
|
||||||
|
|
||||||
- **2.5.0** — единая версия по коду и документации; удалён дефолтный PAT из `bootstrap.sh`; исправлена статистика в боте (CSV header больше не ломает чтение истории, бот сам обновляет snapshot); CLI-смена шаблона теперь обновляет `config.json.template_id`, поэтому бот показывает текущий шаблон; telemt TOML включает локальный API `127.0.0.1:9091` и metrics `127.0.0.1:9090`; добавлено меню Telegram-бота для отдельных ключей пользователей (`[access.users]`): список, добавление, удаление, ссылка и runtime/API-информация; добавлена локальная web-админка на `127.0.0.1:1984` под SSH tunnel; backup/restore сохраняет bot `.env`, языки бота, web-admin token/static, custom templates, stats history и структуру Let's Encrypt для переезда на новый VPS; добавлен безопасный детект 3x-ui/Xray на 443 с предупреждением и заметкой по shared-443.
|
- **2.5.0** — единая версия по коду и документации; удалён дефолтный PAT из `bootstrap.sh`; исправлена статистика в боте (CSV header больше не ломает чтение истории, бот сам обновляет snapshot); CLI-смена шаблона теперь обновляет `config.json.template_id`, поэтому бот показывает текущий шаблон; telemt TOML включает локальный API `127.0.0.1:9091` и metrics `127.0.0.1:9090`; добавлено меню Telegram-бота для отдельных ключей пользователей (`[access.users]`): список, добавление, удаление, ссылка и runtime/API-информация; добавлена локальная web-админка на `127.0.0.1:1984` под SSH tunnel без отдельного токена; backup/restore сохраняет bot `.env`, языки бота, web-admin server/static, custom templates, stats history и структуру Let's Encrypt для переезда на новый VPS; добавлен безопасный детект 3x-ui/Xray на 443 с предупреждением и заметкой по shared-443.
|
||||||
- **2.4.6** — ожидание apt/dpkg lock на свежих Ubuntu/Debian, чтобы установка nginx/certbot/Python не падала во время unattended-upgrades.
|
- **2.4.6** — ожидание apt/dpkg lock на свежих Ubuntu/Debian, чтобы установка nginx/certbot/Python не падала во время unattended-upgrades.
|
||||||
- **2.4.3** — фикс гонки в `bot_action_dispatch`: параллельные вызовы `change-lite-domain`/`change-template` (например, два пользователя бота одновременно) могли получить ошибку «no secret in config», если один процесс читал `config.json` в момент, когда другой его перезаписывал через `jq`. Теперь диспетчер оборачивается в `flock(1)` с таймаутом 30 с; `util-linux` (содержит `flock`) добавлен в критические зависимости.
|
- **2.4.3** — фикс гонки в `bot_action_dispatch`: параллельные вызовы `change-lite-domain`/`change-template` (например, два пользователя бота одновременно) могли получить ошибку «no secret in config», если один процесс читал `config.json` в момент, когда другой его перезаписывал через `jq`. Теперь диспетчер оборачивается в `flock(1)` с таймаутом 30 с; `util-linux` (содержит `flock`) добавлен в критические зависимости.
|
||||||
- **2.4.2** — смена шаблона и домена маскировки **прямо из Telegram-бота** без SSH. Раньше эти пункты меню показывали сообщение «сделай через CLI», теперь бот вызывает `install.sh --action=change-template --json` / `--action=change-lite-domain --json` и разбирает ответ. Плюс: безопасный `safe_edit_message` принимает `disable_web_page_preview`; поле статуса шаблона наконец-то отображается (раньше читалось не из того ключа JSON); полный аудит и автоустановка системных зависимостей при первом запуске (`curl jq openssl git xxd tar dig flock` + опциональные `qrencode bc`); `asyncio.Lock` в боте сериализует параллельные callback'и; валидация tpl\_id (`[A-Za-z0-9_-]{1,64}`) и домена до subprocess.
|
- **2.4.2** — смена шаблона и домена маскировки **прямо из Telegram-бота** без SSH. Раньше эти пункты меню показывали сообщение «сделай через CLI», теперь бот вызывает `install.sh --action=change-template --json` / `--action=change-lite-domain --json` и разбирает ответ. Плюс: безопасный `safe_edit_message` принимает `disable_web_page_preview`; поле статуса шаблона наконец-то отображается (раньше читалось не из того ключа JSON); полный аудит и автоустановка системных зависимостей при первом запуске (`curl jq openssl git xxd tar dig flock` + опциональные `qrencode bc`); `asyncio.Lock` в боте сериализует параллельные callback'и; валидация tpl\_id (`[A-Za-z0-9_-]{1,64}`) и домена до subprocess.
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import csv
|
import csv
|
||||||
import hashlib
|
import hashlib
|
||||||
import http.cookies
|
|
||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
@@ -30,7 +29,6 @@ from typing import Any
|
|||||||
|
|
||||||
ADMIN_DIR = Path(os.getenv("GOTELEGRAM_ADMIN_DIR", "/opt/gotelegram-admin"))
|
ADMIN_DIR = Path(os.getenv("GOTELEGRAM_ADMIN_DIR", "/opt/gotelegram-admin"))
|
||||||
STATIC_DIR = Path(os.getenv("GOTELEGRAM_ADMIN_STATIC", str(ADMIN_DIR / "static")))
|
STATIC_DIR = Path(os.getenv("GOTELEGRAM_ADMIN_STATIC", str(ADMIN_DIR / "static")))
|
||||||
TOKEN_FILE = Path(os.getenv("GOTELEGRAM_ADMIN_TOKEN_FILE", str(ADMIN_DIR / "token")))
|
|
||||||
|
|
||||||
GOTELEGRAM_CONFIG = Path(os.getenv("GOTELEGRAM_CONFIG", "/opt/gotelegram/config.json"))
|
GOTELEGRAM_CONFIG = Path(os.getenv("GOTELEGRAM_CONFIG", "/opt/gotelegram/config.json"))
|
||||||
TELEMT_CONFIG = Path(os.getenv("TELEMT_CONFIG", "/etc/telemt/config.toml"))
|
TELEMT_CONFIG = Path(os.getenv("TELEMT_CONFIG", "/etc/telemt/config.toml"))
|
||||||
@@ -63,21 +61,6 @@ def run(cmd: list[str], timeout: int = 8) -> tuple[int, str, str]:
|
|||||||
return 125, "", str(exc)
|
return 125, "", str(exc)
|
||||||
|
|
||||||
|
|
||||||
def ensure_token() -> str:
|
|
||||||
ADMIN_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
if not TOKEN_FILE.exists() or TOKEN_FILE.stat().st_size < 24:
|
|
||||||
TOKEN_FILE.write_text(secrets.token_urlsafe(36) + "\n", encoding="utf-8")
|
|
||||||
os.chmod(TOKEN_FILE, 0o600)
|
|
||||||
return TOKEN_FILE.read_text(encoding="utf-8").strip()
|
|
||||||
|
|
||||||
|
|
||||||
def current_token() -> str:
|
|
||||||
try:
|
|
||||||
return ensure_token()
|
|
||||||
except OSError:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def load_json(path: Path, fallback: Any = None) -> Any:
|
def load_json(path: Path, fallback: Any = None) -> Any:
|
||||||
try:
|
try:
|
||||||
with path.open("r", encoding="utf-8") as fh:
|
with path.open("r", encoding="utf-8") as fh:
|
||||||
@@ -353,27 +336,6 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
def log_message(self, fmt: str, *args: Any) -> None:
|
def log_message(self, fmt: str, *args: Any) -> None:
|
||||||
print("%s - %s" % (self.address_string(), fmt % args))
|
print("%s - %s" % (self.address_string(), fmt % args))
|
||||||
|
|
||||||
def token_from_request(self) -> str:
|
|
||||||
parsed = urllib.parse.urlparse(self.path)
|
|
||||||
qs = urllib.parse.parse_qs(parsed.query)
|
|
||||||
if qs.get("token", [""])[0]:
|
|
||||||
return qs["token"][0]
|
|
||||||
auth = self.headers.get("Authorization", "")
|
|
||||||
if auth.startswith("Bearer "):
|
|
||||||
return auth[len("Bearer "):].strip()
|
|
||||||
cookie_header = self.headers.get("Cookie", "")
|
|
||||||
if cookie_header:
|
|
||||||
cookie = http.cookies.SimpleCookie()
|
|
||||||
cookie.load(cookie_header)
|
|
||||||
if "gtauth" in cookie:
|
|
||||||
return cookie["gtauth"].value
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def is_authorized(self) -> bool:
|
|
||||||
token = current_token()
|
|
||||||
candidate = self.token_from_request()
|
|
||||||
return bool(token and candidate and secrets.compare_digest(token, candidate))
|
|
||||||
|
|
||||||
def send_json(self, payload: Any, status: int = 200) -> None:
|
def send_json(self, payload: Any, status: int = 200) -> None:
|
||||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||||
self.send_response(status)
|
self.send_response(status)
|
||||||
@@ -394,12 +356,6 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
return {}
|
return {}
|
||||||
return json.loads(self.rfile.read(length).decode("utf-8"))
|
return json.loads(self.rfile.read(length).decode("utf-8"))
|
||||||
|
|
||||||
def require_auth(self) -> bool:
|
|
||||||
if self.is_authorized():
|
|
||||||
return True
|
|
||||||
self.send_error_json(401, "unauthorized")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def require_write_guard(self) -> bool:
|
def require_write_guard(self) -> bool:
|
||||||
if self.command in {"POST", "PUT", "PATCH", "DELETE"} and self.headers.get("X-GoTelegram-Admin") != "1":
|
if self.command in {"POST", "PUT", "PATCH", "DELETE"} and self.headers.get("X-GoTelegram-Admin") != "1":
|
||||||
self.send_error_json(403, "missing write guard")
|
self.send_error_json(403, "missing write guard")
|
||||||
@@ -407,8 +363,6 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def route_get_api(self, parsed: urllib.parse.ParseResult) -> None:
|
def route_get_api(self, parsed: urllib.parse.ParseResult) -> None:
|
||||||
if not self.require_auth():
|
|
||||||
return
|
|
||||||
path = parsed.path
|
path = parsed.path
|
||||||
if path == "/api/overview":
|
if path == "/api/overview":
|
||||||
self.send_json({"ok": True, "data": overview_payload()})
|
self.send_json({"ok": True, "data": overview_payload()})
|
||||||
@@ -437,7 +391,7 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
self.send_error_json(404, "not found")
|
self.send_error_json(404, "not found")
|
||||||
|
|
||||||
def route_post_api(self, parsed: urllib.parse.ParseResult) -> None:
|
def route_post_api(self, parsed: urllib.parse.ParseResult) -> None:
|
||||||
if not self.require_auth() or not self.require_write_guard():
|
if not self.require_write_guard():
|
||||||
return
|
return
|
||||||
path = parsed.path
|
path = parsed.path
|
||||||
try:
|
try:
|
||||||
@@ -480,7 +434,7 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
self.send_error_json(404, "not found")
|
self.send_error_json(404, "not found")
|
||||||
|
|
||||||
def route_delete_api(self, parsed: urllib.parse.ParseResult) -> None:
|
def route_delete_api(self, parsed: urllib.parse.ParseResult) -> None:
|
||||||
if not self.require_auth() or not self.require_write_guard():
|
if not self.require_write_guard():
|
||||||
return
|
return
|
||||||
path = parsed.path
|
path = parsed.path
|
||||||
if not path.startswith("/api/users/"):
|
if not path.startswith("/api/users/"):
|
||||||
@@ -504,15 +458,6 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
self.send_json({"ok": True, "restarted": restarted})
|
self.send_json({"ok": True, "restarted": restarted})
|
||||||
|
|
||||||
def send_static(self, parsed: urllib.parse.ParseResult) -> None:
|
def send_static(self, parsed: urllib.parse.ParseResult) -> None:
|
||||||
qs = urllib.parse.parse_qs(parsed.query)
|
|
||||||
if qs.get("token", [""])[0] and self.is_authorized():
|
|
||||||
self.send_response(302)
|
|
||||||
self.send_header("Location", "/")
|
|
||||||
self.send_header("Set-Cookie", "gtauth=%s; Path=/; HttpOnly; SameSite=Strict" % qs["token"][0])
|
|
||||||
self.send_header("Cache-Control", "no-store")
|
|
||||||
self.end_headers()
|
|
||||||
return
|
|
||||||
|
|
||||||
rel = parsed.path.lstrip("/") or "index.html"
|
rel = parsed.path.lstrip("/") or "index.html"
|
||||||
if rel.startswith("api/") or ".." in rel.split("/"):
|
if rel.startswith("api/") or ".." in rel.split("/"):
|
||||||
self.send_error(404)
|
self.send_error(404)
|
||||||
@@ -558,7 +503,6 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
ensure_token()
|
|
||||||
if not STATIC_DIR.exists():
|
if not STATIC_DIR.exists():
|
||||||
raise SystemExit(f"static dir not found: {STATIC_DIR}")
|
raise SystemExit(f"static dir not found: {STATIC_DIR}")
|
||||||
httpd = ThreadingHTTPServer((HOST, PORT), AdminHandler)
|
httpd = ThreadingHTTPServer((HOST, PORT), AdminHandler)
|
||||||
|
|||||||
@@ -36,10 +36,6 @@ async function api(path, options = {}) {
|
|||||||
};
|
};
|
||||||
if (options.body && !headers["Content-Type"]) headers["Content-Type"] = "application/json";
|
if (options.body && !headers["Content-Type"]) headers["Content-Type"] = "application/json";
|
||||||
const res = await fetch(path, { ...options, headers, credentials: "same-origin" });
|
const res = await fetch(path, { ...options, headers, credentials: "same-origin" });
|
||||||
if (res.status === 401) {
|
|
||||||
$("#authLock").classList.remove("hidden");
|
|
||||||
throw new Error("Unauthorized");
|
|
||||||
}
|
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (!res.ok || data.ok === false) throw new Error(data.error || `HTTP ${res.status}`);
|
if (!res.ok || data.ok === false) throw new Error(data.error || `HTTP ${res.status}`);
|
||||||
return data.data ?? data;
|
return data.data ?? data;
|
||||||
|
|||||||
@@ -7,14 +7,6 @@
|
|||||||
<link rel="stylesheet" href="/styles.css">
|
<link rel="stylesheet" href="/styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="authLock" class="auth-lock hidden">
|
|
||||||
<div class="auth-panel">
|
|
||||||
<div class="mark">GT</div>
|
|
||||||
<h1>GoTelegram Admin</h1>
|
|
||||||
<p>Откройте ссылку из Telegram-бота после запуска SSH-туннеля.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
|
|||||||
@@ -352,34 +352,6 @@ td code {
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-lock {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 20;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
background: rgba(245, 247, 251, .92);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-panel {
|
|
||||||
width: min(420px, calc(100vw - 32px));
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: white;
|
|
||||||
padding: 26px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-panel h1 {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-panel p {
|
|
||||||
margin-top: 8px;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ PROMO_STAMP_FILE = "/opt/gotelegram/.promo_bot_last_shown"
|
|||||||
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||||
ENV_FILE = "/opt/gotelegram-bot/.env"
|
ENV_FILE = "/opt/gotelegram-bot/.env"
|
||||||
ADMIN_WEB_SERVICE = "gotelegram-admin"
|
ADMIN_WEB_SERVICE = "gotelegram-admin"
|
||||||
ADMIN_WEB_TOKEN_FILE = "/opt/gotelegram-admin/token"
|
|
||||||
ADMIN_WEB_PORT = 1984
|
ADMIN_WEB_PORT = 1984
|
||||||
|
|
||||||
# ── Загрузка ALLOWED_IDS ────────────────────────────────────────────────────
|
# ── Загрузка ALLOWED_IDS ────────────────────────────────────────────────────
|
||||||
@@ -2161,14 +2160,6 @@ async def cb_ssl_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
|
|||||||
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
||||||
|
|
||||||
|
|
||||||
def read_admin_web_token() -> str:
|
|
||||||
try:
|
|
||||||
token = Path(ADMIN_WEB_TOKEN_FILE).read_text(encoding="utf-8").strip()
|
|
||||||
return token if len(token) >= 24 else ""
|
|
||||||
except OSError:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
async def admin_web_host_hint() -> str:
|
async def admin_web_host_hint() -> str:
|
||||||
config = load_json(GOTELEGRAM_CONFIG) or {}
|
config = load_json(GOTELEGRAM_CONFIG) or {}
|
||||||
domain = str(config.get("domain") or "")
|
domain = str(config.get("domain") or "")
|
||||||
@@ -2183,18 +2174,15 @@ async def cb_menu_admin_web(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
|||||||
await query.answer()
|
await query.answer()
|
||||||
user_id = _uid(update)
|
user_id = _uid(update)
|
||||||
running = await check_service_status(ADMIN_WEB_SERVICE)
|
running = await check_service_status(ADMIN_WEB_SERVICE)
|
||||||
token = read_admin_web_token()
|
|
||||||
host = await admin_web_host_hint()
|
host = await admin_web_host_hint()
|
||||||
local_url = f"http://127.0.0.1:{ADMIN_WEB_PORT}/?token={token}" if token else f"http://127.0.0.1:{ADMIN_WEB_PORT}/"
|
local_url = f"http://127.0.0.1:{ADMIN_WEB_PORT}/"
|
||||||
ssh_cmd = f"ssh -L {ADMIN_WEB_PORT}:127.0.0.1:{ADMIN_WEB_PORT} root@{host}"
|
ssh_cmd = f"ssh -L {ADMIN_WEB_PORT}:127.0.0.1:{ADMIN_WEB_PORT} root@{host}"
|
||||||
|
|
||||||
if get_user_lang(user_id) == "ru":
|
if get_user_lang(user_id) == "ru":
|
||||||
status = "запущена" if running else "не запущена"
|
status = "запущена" if running else "не запущена"
|
||||||
token_note = "Токен найден." if token else "Токен не найден: переустановите/обновите web-admin через CLI."
|
|
||||||
text = (
|
text = (
|
||||||
f"<b>🖥 Web Admin</b>\n\n"
|
f"<b>🖥 Web Admin</b>\n\n"
|
||||||
f"Статус: <code>{status}</code>\n"
|
f"Статус: <code>{status}</code>\n\n"
|
||||||
f"{html.escape(token_note)}\n\n"
|
|
||||||
"<b>Termius</b>\n"
|
"<b>Termius</b>\n"
|
||||||
"1. Откройте сервер → Port Forwarding.\n"
|
"1. Откройте сервер → Port Forwarding.\n"
|
||||||
f"2. Добавьте Local tunnel: <code>127.0.0.1:{ADMIN_WEB_PORT}</code> → "
|
f"2. Добавьте Local tunnel: <code>127.0.0.1:{ADMIN_WEB_PORT}</code> → "
|
||||||
@@ -2207,11 +2195,9 @@ async def cb_menu_admin_web(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
status = "running" if running else "not running"
|
status = "running" if running else "not running"
|
||||||
token_note = "Token is ready." if token else "Token is missing: reinstall/update web-admin from CLI."
|
|
||||||
text = (
|
text = (
|
||||||
f"<b>🖥 Web Admin</b>\n\n"
|
f"<b>🖥 Web Admin</b>\n\n"
|
||||||
f"Status: <code>{status}</code>\n"
|
f"Status: <code>{status}</code>\n\n"
|
||||||
f"{html.escape(token_note)}\n\n"
|
|
||||||
"<b>Termius</b>\n"
|
"<b>Termius</b>\n"
|
||||||
"1. Open the server → Port Forwarding.\n"
|
"1. Open the server → Port Forwarding.\n"
|
||||||
f"2. Add a Local tunnel: <code>127.0.0.1:{ADMIN_WEB_PORT}</code> → "
|
f"2. Add a Local tunnel: <code>127.0.0.1:{ADMIN_WEB_PORT}</code> → "
|
||||||
|
|||||||
@@ -899,12 +899,7 @@ install_admin_web() {
|
|||||||
cp -a "$src_dir/static/." "$ADMIN_WEB_DIR/static/"
|
cp -a "$src_dir/static/." "$ADMIN_WEB_DIR/static/"
|
||||||
chmod 700 "$ADMIN_WEB_DIR"
|
chmod 700 "$ADMIN_WEB_DIR"
|
||||||
chmod 755 "$ADMIN_WEB_DIR/server.py" "$ADMIN_WEB_DIR/static"
|
chmod 755 "$ADMIN_WEB_DIR/server.py" "$ADMIN_WEB_DIR/static"
|
||||||
|
rm -f "$ADMIN_WEB_DIR/token" 2>/dev/null || true
|
||||||
if [ ! -f "$ADMIN_WEB_DIR/token" ]; then
|
|
||||||
openssl rand -base64 48 | tr -d '\n=+/' | cut -c1-48 > "$ADMIN_WEB_DIR/token"
|
|
||||||
echo "" >> "$ADMIN_WEB_DIR/token"
|
|
||||||
fi
|
|
||||||
chmod 600 "$ADMIN_WEB_DIR/token"
|
|
||||||
|
|
||||||
local python_bin
|
local python_bin
|
||||||
python_bin=$(command -v python3)
|
python_bin=$(command -v python3)
|
||||||
|
|||||||
@@ -123,12 +123,7 @@ if [ -f "$SCRIPT_DIR/admin-web/server.py" ]; then
|
|||||||
cp -a "$SCRIPT_DIR/admin-web/static/." "$ADMIN_WEB_DIR/static/"
|
cp -a "$SCRIPT_DIR/admin-web/static/." "$ADMIN_WEB_DIR/static/"
|
||||||
chmod 700 "$ADMIN_WEB_DIR"
|
chmod 700 "$ADMIN_WEB_DIR"
|
||||||
chmod 755 "$ADMIN_WEB_DIR/server.py" "$ADMIN_WEB_DIR/static"
|
chmod 755 "$ADMIN_WEB_DIR/server.py" "$ADMIN_WEB_DIR/static"
|
||||||
|
rm -f "$ADMIN_WEB_DIR/token" 2>/dev/null || true
|
||||||
if [ ! -f "$ADMIN_WEB_DIR/token" ]; then
|
|
||||||
openssl rand -base64 48 | tr -d '\n=+/' | cut -c1-48 > "$ADMIN_WEB_DIR/token"
|
|
||||||
echo "" >> "$ADMIN_WEB_DIR/token"
|
|
||||||
fi
|
|
||||||
chmod 600 "$ADMIN_WEB_DIR/token"
|
|
||||||
|
|
||||||
PYTHON_BIN=$(command -v python3)
|
PYTHON_BIN=$(command -v python3)
|
||||||
cat > "/etc/systemd/system/${ADMIN_WEB_SERVICE}.service" << EOF
|
cat > "/etc/systemd/system/${ADMIN_WEB_SERVICE}.service" << EOF
|
||||||
|
|||||||
@@ -72,10 +72,9 @@ create_backup() {
|
|||||||
[ -d "$BOT_DIR/lang" ] && cp -a "$BOT_DIR/lang" "$tmp_dir/bot/" 2>/dev/null
|
[ -d "$BOT_DIR/lang" ] && cp -a "$BOT_DIR/lang" "$tmp_dir/bot/" 2>/dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Local web admin state (token is needed so restored bots can show the same tunnel URL)
|
# Local web admin state
|
||||||
if [ -d "$ADMIN_WEB_DIR" ]; then
|
if [ -d "$ADMIN_WEB_DIR" ]; then
|
||||||
mkdir -p "$tmp_dir/admin_web"
|
mkdir -p "$tmp_dir/admin_web"
|
||||||
[ -f "$ADMIN_WEB_DIR/token" ] && cp "$ADMIN_WEB_DIR/token" "$tmp_dir/admin_web/token" 2>/dev/null
|
|
||||||
[ -f "$ADMIN_WEB_DIR/server.py" ] && cp "$ADMIN_WEB_DIR/server.py" "$tmp_dir/admin_web/server.py" 2>/dev/null
|
[ -f "$ADMIN_WEB_DIR/server.py" ] && cp "$ADMIN_WEB_DIR/server.py" "$tmp_dir/admin_web/server.py" 2>/dev/null
|
||||||
[ -d "$ADMIN_WEB_DIR/static" ] && cp -a "$ADMIN_WEB_DIR/static" "$tmp_dir/admin_web/" 2>/dev/null
|
[ -d "$ADMIN_WEB_DIR/static" ] && cp -a "$ADMIN_WEB_DIR/static" "$tmp_dir/admin_web/" 2>/dev/null
|
||||||
fi
|
fi
|
||||||
@@ -316,9 +315,9 @@ restore_backup() {
|
|||||||
# Восстанавливаем состояние локальной web-админки
|
# Восстанавливаем состояние локальной web-админки
|
||||||
if [ -d "$backup_dir/admin_web" ]; then
|
if [ -d "$backup_dir/admin_web" ]; then
|
||||||
mkdir -p "$ADMIN_WEB_DIR"
|
mkdir -p "$ADMIN_WEB_DIR"
|
||||||
[ -f "$backup_dir/admin_web/token" ] && cp "$backup_dir/admin_web/token" "$ADMIN_WEB_DIR/token" 2>/dev/null && chmod 600 "$ADMIN_WEB_DIR/token"
|
|
||||||
[ -f "$backup_dir/admin_web/server.py" ] && cp "$backup_dir/admin_web/server.py" "$ADMIN_WEB_DIR/server.py" 2>/dev/null
|
[ -f "$backup_dir/admin_web/server.py" ] && cp "$backup_dir/admin_web/server.py" "$ADMIN_WEB_DIR/server.py" 2>/dev/null
|
||||||
[ -d "$backup_dir/admin_web/static" ] && cp -a "$backup_dir/admin_web/static" "$ADMIN_WEB_DIR/" 2>/dev/null
|
[ -d "$backup_dir/admin_web/static" ] && cp -a "$backup_dir/admin_web/static" "$ADMIN_WEB_DIR/" 2>/dev/null
|
||||||
|
rm -f "$ADMIN_WEB_DIR/token" 2>/dev/null || true
|
||||||
log_success "Конфигурация web-админки восстановлена"
|
log_success "Конфигурация web-админки восстановлена"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user