diff --git a/README.md b/README.md index 2a6632f..bc541e7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # amnezia_web-PRO -Веб-панель на вашем VPS для управления клиентами **AmneziaWG**: вкл/выкл, удаление, переименование, дата отключения; при нескольких контейнерах — переключатель **«Инстанс»** (`AWG_PROFILES`); **экспорт .conf** при наличии `last_config`; **новый клиент под каскад** (свой Endpoint и ключи на сервере). **Cloudflare WARP** — *необязательное* дополнение: часть клиентов может выходить в интернет через интерфейс `warp` внутри контейнера AWG (ставится отдельно скриптом на хосте). Работает через Docker и `docker exec` в контейнер **Amnezia** (по умолчанию `amnezia-awg2`). +Веб-панель на вашем VPS для управления клиентами **AmneziaWG**: вкл/выкл, удаление, переименование, дата отключения; при нескольких контейнерах — переключатель **«Инстанс»** (`AWG_PROFILES`); **экспорт .conf** при наличии `last_config`; **новый клиент под каскад** (свой Endpoint и ключи на сервере); **Telegram MTProto‑прокси** (отдельный контейнер `telegrammessenger/proxy`, управление из панели). **Cloudflare WARP** — *необязательное* дополнение: часть клиентов может выходить в интернет через интерфейс `warp` внутри контейнера AWG (ставится отдельно скриптом на хосте). Работает через Docker и `docker exec` в контейнер **Amnezia** (по умолчанию `amnezia-awg2`). Справочник по интерфейсу, типичным сбоям и HTTP API: **[docs/panel-guide.md](docs/panel-guide.md)**. @@ -8,7 +8,7 @@ ### Редакции: открытая база и PRO -- **[amnezia_web](https://github.com/andrey271192/amnezia_web)** — открытый репозиторий **базовой** панели: **только просмотр** клиентов AmneziaWG и статусов. Нет включения/выключения peer в туннеле, правки дат отключения, переименования, удаления, экспорта `.conf`, блока «Новый клиент под каскад», Cloudflare WARP и синхронизации времени хоста по SSH. Редакция задаётся файлом **`.amnezia-panel-edition`** со значением `community` (его подставляет установщик этого форка) или переменной **`AMNEZIA_EDITION=community`**. Текст и кнопка «Разблокировать PRO» используют **`COMMUNITY_UPGRADE_URL`** и **`COMMUNITY_UPGRADE_PITCH`** (по умолчанию URL ведёт на страницу подписки уровня PRO на Boosty); полный код и приватный репозиторий **amnezia_web-PRO** подключаются подписчикам отдельно. +- **[amnezia_web](https://github.com/andrey271192/amnezia_web)** — открытый репозиторий **базовой** панели: **только просмотр** клиентов AmneziaWG и статусов. Нет включения/выключения peer в туннеле, правки дат отключения, переименования, удаления, экспорта `.conf`, блока «Новый клиент под каскад», Cloudflare WARP, Telegram MTProto‑прокси и синхронизации времени хоста по SSH. Редакция задаётся файлом **`.amnezia-panel-edition`** со значением `community` (его подставляет установщик этого форка) или переменной **`AMNEZIA_EDITION=community`**. Текст и кнопка «Разблокировать PRO» используют **`COMMUNITY_UPGRADE_URL`** и **`COMMUNITY_UPGRADE_PITCH`** (по умолчанию URL ведёт на страницу подписки уровня PRO на Boosty); полный код и приватный репозиторий **amnezia_web-PRO** подключаются подписчикам отдельно. - **Этот репозиторий (`amnezia_web-PRO`)** — **полная** панель (редакция **pro** по умолчанию): все функции из README ниже активны, если не задана **`AMNEZIA_EDITION=community`**. @@ -16,7 +16,7 @@ | Адрес | Назначение | |-------|------------| -| **`http://IP:HOST_PORT`** (по умолчанию **`:8080`**) | **Админ-панель:** клиенты AmneziaWG, опциональный блок **Cloudflare WARP**, каскад, экспорт `.conf`, смена пароля. Внизу страницы — футер со ссылками автора (GitHub, донат, Telegram и т.д.). | +| **`http://IP:HOST_PORT`** (по умолчанию **`:8080`**) | **Админ-панель:** клиенты AmneziaWG, опциональные блоки **Telegram MTProto‑прокси** и **Cloudflare WARP**, каскад, экспорт `.conf`, смена пароля. Внизу страницы — футер со ссылками автора (GitHub, донат, Telegram и т.д.). | | **`http://IP:LANDING_PORT`** (по умолчанию **`:80`**) | **Публичная страница** для приглашённых: инструкция, кнопка перехода в админку (`landing/admin-port.js` подставляет тот же `HOST_PORT`). Текст «проблемы — администратору сервера» и дисклеймер. **Без** блока доната и личных ссылок автора — они только в админке. | Если порт **80** занят другим сервисом, задайте **`LANDING_PORT`** (например `8081`) или **`SKIP_LANDING=1`**. На работу панели по **`HOST_PORT`** это не влияет. @@ -105,10 +105,17 @@ cd /opt/amnezia-admin && chmod +x scripts/install.sh && sudo SKIP_DOWNLOAD=1 bas | `LANDING_PORT` | `80` | Порт nginx-лендинга (если `80` занят — например `8081`) | | `LANDING_CONTAINER` | `amnezia-web-landing` | Имя контейнера лендинга | | `NO_CACHE` | `0` | `1` — `docker build --no-cache` при проблемах с обновлением образа | -| `UI_HIDE_SECTIONS` | _(нет)_ | Список через запятую: **`users`**, **`warp`**, **`cascade`** — скрыть блоки в веб-панели (см. ниже). При обновлении через `install.sh` значение подтягивается из старого контейнера, если не задано заново | +| `UI_HIDE_SECTIONS` | _(нет)_ | Список через запятую: **`users`**, **`warp`**, **`cascade`**, **`mtproto`** — скрыть блоки в веб-панели (см. ниже). При обновлении через `install.sh` значение подтягивается из старого контейнера, если не задано заново | | `UI_HIDE_USERS` | `0` | `1` или `true` — эквивалент `users` в `UI_HIDE_SECTIONS` | | `UI_HIDE_WARP` | `0` | `1` — эквивалент `warp` (также блокируются `POST /api/warp/*`) | | `UI_HIDE_CASCADE` | `0` | `1` — эквивалент `cascade` (блокируется `POST /api/clients/create-cascade`) | +| `UI_HIDE_MTPROTO` | `0` | `1` или **`mtproto`** в `UI_HIDE_SECTIONS` — скрыть блок установки MTProto; **`GET/POST /api/mtproto/*`** отвечают **403** | +| `MTPRO_PROXY_CONTAINER` | `mtproto-proxy` | Имя контейнера MTProto (образ **telegrammessenger/proxy**) | +| `MTPRO_PROXY_IMAGE` | `telegrammessenger/proxy:latest` | Образ **`docker pull`** при установке из панели | +| `MTPRO_INTERNAL_PORT` | `443` | Порт процесса **внутри** контейнера прокси | +| `MTPRO_PUBLISH_PORT` | `8443` | Порт **хоста**, проброшенный наружу (`-p`) | +| `MTPRO_PUBLISH_BIND` | `0.0.0.0` | Адрес bind на хосте для `-p` | +| `MTPRO_PUBLIC_HOST` | _(нет)_ | Публичный IP/DNS для ссылки **`tg://proxy`**; можно вместо этого задать **`CLIENT_CONFIG_ENDPOINT`** | Переменная **`AWG_PROFILES`** при установке автоматически сохраняется в **`/root/amnezia-admin.awg-profiles.json`** на VPS; при следующем запуске `install.sh` без `AWG_PROFILES` значение подставляется из этого файла или из **старого контейнера** `amnezia-admin` перед его удалением — так переключатель «Инстанс» не пропадает после обновления панели. @@ -136,6 +143,18 @@ curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/s Если на хосте уже запущено **несколько** контейнеров с именами вида **`amnezia-awg*`**, а в панели по-прежнему один инстанс — задайте **`AWG_PROFILES`** (или файл **`/root/amnezia-admin.awg-profiles.json`**) и перезапустите установщик; при запуске **`install.sh`** без профилей в этом случае выводится предупреждение в консоль. +### Telegram MTProto‑прокси + +**Не связано с AmneziaWG.** Отдельный контейнер образа **[telegrammessenger/proxy](https://hub.docker.com/r/telegrammessenger/proxy)** на том же хосте. После входа в панель: установка, перезапуск, удаление, выбор порта на хосте, просмотр логов и ссылки **`tg://proxy`** (хост из **`MTPRO_PUBLIC_HOST`**, **`CLIENT_CONFIG_ENDPOINT`** или из URL страницы панели). + +- **`GET /api/mtproto/status`** — состояние и ссылка; **`?withLogs=1`** добавляет хвост логов (**`logsFetched`**). +- **`GET /api/mtproto/logs`** и **`GET /api/mtproto/tail`** — хвост **`docker logs`** (отдельный путь **`/tail`**, если прокси обрезает путь **`…/logs`**). +- **`POST /api/mtproto/install`** | **`…/restart`** | **`…/remove`** + +За nginx/Caddy нужно проксировать **весь** префикс **`/api/`** на процесс панели. После сборки образа можно свериться: **`GET /health`** отдаёт **`version`** из `package.json`. + +Чтобы **скрыть** блок и отключить API: **`UI_HIDE_MTPROTO=1`** или токен **`mtproto`** в **`UI_HIDE_SECTIONS`**. + ### Cloudflare WARP (необязательно) **Устанавливать не обязательно.** Панель и обычный AmneziaWG работают без WARP. Статус **«Не установлен»** значит: в контейнере AWG ещё **нет** `warp.conf` (скрипт на VPS не запускали) — это не ошибка. @@ -183,6 +202,7 @@ cd /opt/amnezia-admin | `users` | Секция **«Пользователи»** (таблица) и отладочный вывод **awg show**. Запрос **`GET /api/clients`** по-прежнему нужен странице — API для вкл/выкл peer и экспорта **не отключается**, скрыта только таблица. | | `warp` | Блок **Cloudflare WARP**; запросы **`POST /api/warp/*`** отвечают **403**. | | `cascade` | Блок **«Новый клиент под каскад»**; **`POST /api/clients/create-cascade`** отвечает **403**. | +| `mtproto` | Блок **«Telegram MTProto‑прокси»**; **`GET`/`POST /api/mtproto/*`** отвечают **403**. | Пример установки / обновления с частично пустой панелью: @@ -190,7 +210,7 @@ cd /opt/amnezia-admin UI_HIDE_SECTIONS=warp,cascade curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web-PRO/main/scripts/install.sh | sudo -E bash ``` -Отдельные флаги **`UI_HIDE_USERS`**, **`UI_HIDE_WARP`**, **`UI_HIDE_CASCADE`** (`1` или `true`) дублируют соответствующий токен. +Отдельные флаги **`UI_HIDE_USERS`**, **`UI_HIDE_WARP`**, **`UI_HIDE_CASCADE`**, **`UI_HIDE_MTPROTO`** (`1` или `true`) дублируют соответствующий токен. Итоговые URL после установки совпадают с таблицей **«Что открывается по какому порту»** в начале этого файла; установщик записывает выбранный `HOST_PORT` в **`landing/admin-port.js`**, чтобы кнопка на лендинге вела на нужную админку. diff --git a/docs/panel-guide.md b/docs/panel-guide.md index aa898ef..fa9ab03 100644 --- a/docs/panel-guide.md +++ b/docs/panel-guide.md @@ -9,6 +9,7 @@ | **Инстанс** | Переключение между профилями из `AWG_PROFILES` (разные контейнеры AmneziaWG / Legacy). Виден только если в контейнере панели задан JSON из **двух и более** профилей. | | **Время** | Отображение часов контейнера и браузера; синхронизация времени хоста VPS по SSH (если доступно). | | **Cloudflare WARP** | *Необязательно.* Вывод части клиентов в интернет через интерфейс `warp` в контейнере AWG. Если WARP не ставили — статус **«Не установлен»** нормален; панель и VPN без этого работают. Установка и удаление — скрипт `scripts/warp-amnezia.sh` на хосте (`install` / **`uninstall`**), подробности в основном [README](../README.md). | +| **Telegram MTProto‑прокси** | *Необязательно*, не часть AWG. Docker-контейнер официального образа **telegrammessenger/proxy**; установка из панели, ссылка **`tg://proxy`**, переменные **`MTPRO_*`** — см. основной [README](../README.md). | | **Новый клиент под каскад** | Создание нового peer на сервере с **вашим Endpoint** (промежуточный узел); выдача готового `.conf`. | | **Пользователи** | Вкл/выкл peer, удаление, переименование, даты отключения; экспорт `.conf` если в записи есть `userData.last_config`. | @@ -44,11 +45,12 @@ ## Скрытие разделов в панели (`UI_HIDE_*`) -Переменные контейнера **`amnezia-admin`**: **`UI_HIDE_SECTIONS`** (список `users`, `warp`, `cascade`) или отдельно **`UI_HIDE_USERS`**, **`UI_HIDE_WARP`**, **`UI_HIDE_CASCADE`** (`1` / `true`). Подробности и пример — в таблице установки и разделе README про **`UI_HIDE_*`**. +Переменные контейнера **`amnezia-admin`**: **`UI_HIDE_SECTIONS`** (список `users`, `warp`, `cascade`, **`mtproto`**) или отдельно **`UI_HIDE_USERS`**, **`UI_HIDE_WARP`**, **`UI_HIDE_CASCADE`**, **`UI_HIDE_MTPROTO`** (`1` / `true`). Подробности и пример — в таблице установки и разделе README про **`UI_HIDE_*`**. - **`warp`** — скрывает блок WARP; **`POST /api/warp/*`** → 403. - **`cascade`** — скрывает каскад; **`POST /api/clients/create-cascade`** → 403. - **`users`** — скрывает таблицу «Пользователи» и отладку **awg show**; **`GET /api/clients`** и остальные операции с клиентами **остаются** (скрыт только UI таблицы). +- **`mtproto`** или **`UI_HIDE_MTPROTO`** — скрывает блок MTProto; **`GET`/`POST /api/mtproto/*`** → 403. ## Конфигурации клиентов @@ -67,7 +69,7 @@ | Метод | Путь | Назначение | |-------|------|------------| -| GET | `/health` | Проверка живости | +| GET | `/health` | Проверка живости; JSON **`{ ok, version }`** — **`version`** из `package.json` образа | | GET | `/api/session` | Есть ли действующая сессия | | POST | `/api/login` | `{ "password": "…" }` | | POST | `/api/logout` | Выход | @@ -86,6 +88,12 @@ | POST | `/api/warp/start` | Поднять WARP | | POST | `/api/warp/stop` | Остановить WARP | | POST | `/api/warp/routing` | Политика по клиентам | +| GET | `/api/mtproto/status` | Состояние MTProto‑контейнера и **`tg://proxy`**; **`?withLogs=1`** — хвост логов (**`logsFetched`**) | +| GET | `/api/mtproto/logs` | Хвост **`docker logs`** прокси | +| GET | `/api/mtproto/tail` | То же для прокси, обрезающего путь **`…/logs`** | +| POST | `/api/mtproto/install` | Установить/обновить контейнер (тело `{}`) | +| POST | `/api/mtproto/restart` | Перезапустить контейнер | +| POST | `/api/mtproto/remove` | Удалить контейнер | | GET | `/api/server-time` | Время и подсказки по поясам | | GET | `/api/time-sync-capabilities` | Доступность синхронизации по SSH | | POST | `/api/sync-host-time` | Запись времени на хост через SSH | diff --git a/package.json b/package.json index a8c3b87..069ca18 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amnezia-admin", - "version": "1.1.7", + "version": "1.2.0", "private": true, "description": "amnezia_web-PRO — веб-панель AmneziaWG: несколько инстансов, WARP, каскад/Endpoint, экспорт .conf (Docker)", "license": "MIT", diff --git a/public/app.js b/public/app.js index 5872d46..f2bf772 100644 --- a/public/app.js +++ b/public/app.js @@ -21,6 +21,19 @@ const warpActionsEl = document.querySelector("#warp-actions"); const warpClientListEl = document.querySelector("#warp-client-list"); const warpWgShowEl = document.querySelector("#warp-wg-show"); +const mtprotoPanel = document.querySelector("#mtproto-panel"); +const mtprotoStatusLine = document.querySelector("#mtproto-status-line"); +const mtprotoActionsEl = document.querySelector("#mtproto-actions"); +const mtprotoLogsTailEl = document.querySelector("#mtproto-logs-tail"); +const mtprotoBanner = document.querySelector("#mtproto-banner"); +const mtprotoHostPortInput = document.querySelector("#mtproto-host-port"); +const mtprotoSecretInput = document.querySelector("#mtproto-secret-opt"); +const mtprotoCalloutEl = document.querySelector("#mtproto-callout"); +const mtprotoLinkCardEl = document.querySelector("#mtproto-link-card"); +const mtprotoLinkAnchorEl = document.querySelector("#mtproto-link-anchor"); +const mtprotoLinkCopyBtn = document.querySelector("#mtproto-link-copy"); +const mtprotoLinkCopyState = document.querySelector("#mtproto-link-copy-state"); + const cascadePanel = document.querySelector("#cascade-panel"); const usersPanel = document.querySelector("#users-panel"); const wgRawDetails = document.querySelector("#wg-raw-details"); @@ -58,7 +71,7 @@ const warpSshErr = document.querySelector("#warp-ssh-err"); let warpSshPendingCmd = null; /** Какие панели скрыты настройкой сервера (`UI_HIDE_SECTIONS`). */ -let uiHidden = { users: false, warp: false, cascade: false }; +let uiHidden = { users: false, warp: false, cascade: false, mtproto: false }; const editionBanner = document.querySelector("#edition-banner"); const DEFAULT_HEADER_SUB = document.querySelector(".top .sub")?.textContent?.trim() || ""; @@ -93,7 +106,7 @@ function applyEditionPayload(data) { if (subEl) { if (editionState.tier === "community") { subEl.textContent = - "Базовая панель amnezia_web: только просмотр клиентов AmneziaWG и статусов. Управление туннелем, экспорт .conf, каскад, Cloudflare WARP и синхронизация времени хоста — в версии PRO."; + "Базовая панель amnezia_web: только просмотр клиентов AmneziaWG и статусов. Управление туннелем, экспорт .conf, каскад, Cloudflare WARP, MTProto‑прокси Telegram и синхронизация времени хоста — в версии PRO."; } else { subEl.textContent = DEFAULT_HEADER_SUB; } @@ -131,6 +144,200 @@ function applyEditionPayload(data) { if (wgRawDetails) wgRawDetails.hidden = uiHidden.users || !editionState.showDebugWg; } +/** @param {string} [tgLink] */ +function setMtprotoLinkCard(tgLink) { + const t = typeof tgLink === "string" ? tgLink.trim() : ""; + if (!mtprotoLinkCardEl) return; + if (!t) { + mtprotoLinkCardEl.classList.add("hidden"); + delete mtprotoLinkCardEl.dataset.tgLink; + if (mtprotoLinkAnchorEl) { + mtprotoLinkAnchorEl.removeAttribute("href"); + mtprotoLinkAnchorEl.textContent = ""; + } + return; + } + mtprotoLinkCardEl.classList.remove("hidden"); + mtprotoLinkCardEl.dataset.tgLink = t; + if (mtprotoLinkAnchorEl) { + mtprotoLinkAnchorEl.href = t; + mtprotoLinkAnchorEl.textContent = t; + } + if (mtprotoLinkCopyState) mtprotoLinkCopyState.textContent = ""; +} + +async function refreshMtprotoPanel() { + if (!mtprotoPanel || !mtprotoStatusLine || !mtprotoActionsEl) return; + if (uiHidden.mtproto) { + mtprotoPanel.hidden = true; + if (mtprotoCalloutEl) { + mtprotoCalloutEl.textContent = ""; + mtprotoCalloutEl.classList.add("hidden"); + } + setMtprotoLinkCard(""); + return; + } + mtprotoPanel.hidden = false; + mtprotoActionsEl.innerHTML = ""; + if (mtprotoBanner) { + mtprotoBanner.classList.add("hidden"); + mtprotoBanner.textContent = ""; + } + try { + if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = "Загрузка логов…"; + const snap = await api("/api/mtproto/status?withLogs=1"); + if (snap.logsFetched !== true && snap.exists === true && snap.running === true) { + void (async () => { + try { + const lg = + (await api("/api/mtproto/tail").catch(() => api("/api/mtproto/logs"))) || + ({ logsTail: "" }); + const tail = typeof lg?.logsTail === "string" ? lg.logsTail : ""; + if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = tail; + } catch { + if (mtprotoLogsTailEl) { + mtprotoLogsTailEl.textContent = "(лог не загрузился — обновите раздел позже)"; + } + } + })(); + } else { + const tail = typeof snap.logsTail === "string" ? snap.logsTail : ""; + if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = tail; + } + + if (mtprotoCalloutEl) { + if (snap.exists) { + const hp = + snap.hostPort != null && snap.hostPort !== "" ? String(snap.hostPort) : null; + mtprotoCalloutEl.classList.remove("hidden"); + mtprotoCalloutEl.textContent = hp + ? `Официальный образ слушает 443/tcp внутри контейнера — это норма. На хост проброшен порт ${hp}: в Telegram указывайте публичный IP/DNS этого VPS и порт ${hp}. В логах ниже официальный прокси сам пишет tg:// и port=443 (внутренний процесс); при таком пробросе в клиенте используйте порт ${hp}, а не 443. Ссылку из синего блока панели (если показана) можно открыть как есть.` + : `Официальный образ слушает 443/tcp внутри контейнера; снаружи — порт хоста из строки статуса («порт хоста»). Если в логах видно tg:// с port=443, для подключения с интернета подставляйте порт хоста из статуса, не 443.`; + } else { + mtprotoCalloutEl.textContent = ""; + mtprotoCalloutEl.classList.add("hidden"); + } + } + + const parts = []; + if (snap.exists) parts.push("контейнер есть"); + if (snap.running) parts.push("запущен"); + mtprotoStatusLine.textContent = + parts.length > 0 + ? `${parts.join(" · ")}${ + snap.hostPort ? ` · порт хоста :${String(snap.hostPort)}` : "" + }${snap.secretMasked ? ` · секрет ${snap.secretMasked}` : ""}` + : "не установлен"; + + if (typeof snap.hint === "string" && snap.hint.trim()) { + const hintEl = document.createElement("p"); + hintEl.className = "muted warp-muted"; + hintEl.textContent = snap.hint.trim(); + mtprotoActionsEl.appendChild(hintEl); + } else if (snap.exists && !snap.running && typeof snap.image === "string" && snap.image.trim()) { + const imgHint = document.createElement("p"); + imgHint.className = "muted warp-muted"; + imgHint.textContent = `После установки образ будет доступен здесь (${snap.image.trim()} при актуальной конфигурации).`; + mtprotoActionsEl.appendChild(imgHint); + } + + setMtprotoLinkCard(snap.tgLink); + + const row = document.createElement("div"); + row.className = "warp-actions"; + + row.appendChild( + btn("Установить / пересоздать", "btn small primary", async () => { + try { + setStatus("MTProto: docker pull/run…", false); + const hp = mtprotoHostPortInput?.value?.trim(); + const sec = mtprotoSecretInput?.value?.trim()?.toLowerCase(); + const body = {}; + if (hp !== "" && hp !== undefined) { + const n = Number.parseInt(String(hp), 10); + if (Number.isFinite(n)) body.hostPort = n; + } + if (/^[a-f0-9]{32}$/.test(sec)) body.secret = sec; + const j = await api("/api/mtproto/install", { method: "POST", body: JSON.stringify(body) }); + if (typeof j.tgLink === "string" && j.tgLink.trim()) setMtprotoLinkCard(j.tgLink.trim()); + const lines = []; + if (typeof j.secretHex === "string") lines.push(`Секрет (сохраните): ${j.secretHex}`); + if (typeof j.tgLink === "string" && j.tgLink) lines.push(`Ссылка: ${j.tgLink}`); + if (mtprotoBanner) { + mtprotoBanner.textContent = lines.join("\n"); + mtprotoBanner.classList.remove("hidden"); + } + if (typeof j.secretHex === "string" && mtprotoSecretInput) mtprotoSecretInput.value = ""; + await refreshMtprotoPanel(); + setStatus("MTProto: готово", false); + } catch (e) { + setStatus(String(e?.message || e), true); + } + }), + ); + + row.appendChild( + btn("Перезапустить", "btn small ghost", async () => { + try { + setStatus("MTProto: перезапуск…", false); + await api("/api/mtproto/restart", { method: "POST", body: JSON.stringify({}) }); + await refreshMtprotoPanel(); + setStatus("", false); + } catch (e) { + setStatus(String(e?.message || e), true); + } + }), + ); + + row.appendChild( + btn("Удалить прокси", "btn small warn", async () => { + try { + if ( + !confirm( + "Удалить контейнер MTProto? Секрет в Telegram сохранять не нужно — он пересоздастся заново при установке.", + ) + ) + return; + setStatus("MTProto: удаление…", false); + await api("/api/mtproto/remove", { method: "POST", body: JSON.stringify({}) }); + if (mtprotoBanner) { + mtprotoBanner.classList.add("hidden"); + mtprotoBanner.textContent = ""; + } + await refreshMtprotoPanel(); + setStatus("", false); + } catch (e) { + setStatus(String(e?.message || e), true); + } + }), + ); + + row.appendChild( + btn("Обновить статус", "btn small ghost", async () => { + try { + setStatus("", false); + await refreshMtprotoPanel(); + } catch (e) { + setStatus(String(e?.message || e), true); + } + }), + ); + + mtprotoActionsEl.appendChild(row); + } catch (e) { + if (mtprotoCalloutEl) { + mtprotoCalloutEl.textContent = ""; + mtprotoCalloutEl.classList.add("hidden"); + } + setMtprotoLinkCard(""); + if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = ""; + const err = document.createElement("p"); + err.className = "muted warp-muted err"; + err.textContent = String(e.message || e); + mtprotoActionsEl.appendChild(err); + } +} + function applyUiHiddenFromPayload(data) { const u = data?.uiHidden; if (u && typeof u === "object") { @@ -138,11 +345,15 @@ function applyUiHiddenFromPayload(data) { users: Boolean(u.users), warp: Boolean(u.warp), cascade: Boolean(u.cascade), + mtproto: Boolean(u.mtproto), }; } if (usersPanel) usersPanel.hidden = uiHidden.users; if (wgRawDetails) wgRawDetails.hidden = uiHidden.users || !editionState.showDebugWg; if (cascadePanel) cascadePanel.hidden = uiHidden.cascade; + if (warpPanel) warpPanel.hidden = uiHidden.warp; + if (mtprotoPanel) mtprotoPanel.hidden = uiHidden.mtproto; + void refreshMtprotoPanel(); } let dtMode = "disable"; @@ -326,6 +537,8 @@ function setPwMsg(text, isErr) { } async function api(path, opts = {}) { + const pathOnly = + typeof path === "string" ? path.trim().replace(/\?.*$/, "") || String(path).trim() : ""; const res = await fetch(path, { ...opts, credentials: "same-origin", @@ -342,7 +555,19 @@ async function api(path, opts = {}) { data = { raw: text }; } if (!res.ok) { - const msg = data.error || data.raw || res.statusText; + let msg = typeof data.error === "string" ? data.error : ""; + if (!msg) msg = typeof data.raw === "string" ? data.raw : ""; + if (!msg) msg = res.statusText; + const hint = typeof data.hint === "string" && data.hint.trim() ? data.hint.trim() : ""; + if (hint) { + msg = msg ? `${msg} — ${hint}` : hint; + } else if ( + res.status === 404 && + /^\/api\/mtproto\b/.test(pathOnly) && + /^not\s*found\s*$/i.test(String(msg).trim()) + ) { + msg = `${msg.trim()} — проверьте GET /health (version): образ панели должен включать GET /api/mtproto/status, а прокси — весь /api/.`; + } throw new Error(msg); } return data; @@ -871,6 +1096,14 @@ function renderWarpPanel(data) { warpStatusLine.textContent = parts.join(" · "); const selection = new Set((w.selectedAllowedIps || []).map(String)); + const validWarpPeerIps = new Set(); + for (const c of data.clients) { + if (!c.activeInConf) continue; + parseIpv4Cidrs(c.allowedIps).forEach((ip) => validWarpPeerIps.add(ip)); + } + for (const ip of [...selection]) { + if (!validWarpPeerIps.has(ip)) selection.delete(ip); + } function redrawChecks() { warpClientListEl.innerHTML = ""; @@ -1187,9 +1420,44 @@ async function loadClients() { wgShowEl.textContent = ""; peerCountEl.textContent = ""; if (warpPanel) warpPanel.hidden = true; + if (mtprotoPanel) mtprotoPanel.hidden = true; } } +function initMtprotoLinkCopy() { + if (!mtprotoLinkCopyBtn) return; + mtprotoLinkCopyBtn.addEventListener("click", async () => { + const link = + mtprotoLinkCardEl?.dataset?.tgLink || + mtprotoLinkAnchorEl?.href || + ""; + if (!link) return; + try { + if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(link); + else { + const ta = document.createElement("textarea"); + ta.value = link; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + document.execCommand("copy"); + ta.remove(); + } + if (mtprotoLinkCopyState) { + mtprotoLinkCopyState.textContent = "Скопировано"; + setTimeout(() => { + if (mtprotoLinkCopyState) mtprotoLinkCopyState.textContent = ""; + }, 2400); + } + } catch { + if (mtprotoLinkCopyState) { + mtprotoLinkCopyState.textContent = "Не удалось скопировать — выделите вручную."; + } + } + }); +} + async function boot() { const ok = await checkSession(); if (ok) { @@ -1203,4 +1471,5 @@ async function boot() { } } +initMtprotoLinkCopy(); boot(); diff --git a/public/index.html b/public/index.html index 1c621b4..64ece00 100644 --- a/public/index.html +++ b/public/index.html @@ -29,7 +29,7 @@
Панель сервера
Дату отключения задаёте сами (локальное время браузера). Кнопка «Выключить» убирает клиента из туннеля сразу. «Задать дату» без галочки — только запись в таблице; с галочкой — клиент остаётся в туннеле до наступления времени. Несколько инстансов задаются переменной AWG_PROFILES; установщик при апдейте подставляет её из прежнего контейнера или из /root/amnezia-admin.awg-profiles.json. Конфиг .conf — только если на сервере есть last_config; иначе используйте приложение Amnezia.
Дату отключения задаёте сами (локальное время браузера). Кнопка «Выключить» убирает клиента из туннеля сразу. «Задать дату» без галочки — только запись в таблице; с галочкой — клиент остаётся в туннеле до наступления времени. Несколько инстансов задаются переменной AWG_PROFILES; установщик при апдейте подставляет её из прежнего контейнера или из /root/amnezia-admin.awg-profiles.json. Конфиг .conf — только если на сервере есть last_config; иначе используйте приложение Amnezia. Отдельно: раздел Telegram MTProto‑прокси ставит официальный Docker‑образ telegrammessenger/proxy через панель.
+ Отдельный Docker‑контейнер (telegrammessenger/proxy), публикация порта на хосте VPS.
+ Ссылка tg://proxy?… подставляет хост из URL панели, если не заданы переменные; для постоянного IP/DNS укажите MTPRO_PUBLIC_HOST или CLIENT_CONFIG_ENDPOINT.
+ Секрет после установки сохраните вне панели — здесь он показывается маскированно.
+ За nginx/Caddy проксируйте префикс /api/ целиком (см. README).
+