diff --git a/README.md b/README.md index 0dd6d61..932f23b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ cd /opt/amnezia-admin && chmod +x scripts/install.sh && sudo SKIP_DOWNLOAD=1 bas | `CLIENT_CONFIG_ENDPOINT` | _(нет)_ | Публичный IP или DNS VPS для строки `Endpoint` при сборке `.conf` из `last_config`, если нет `hostName` в JSON и заходите в панель не по IP | | `CLIENT_EXPORT_DNS1` | `8.8.8.8` | DNS в экспортируемом клиентском конфиге | | `CLIENT_EXPORT_DNS2` | `8.8.4.4` | Второй DNS в экспортируемом конфиге | +| `EXPORT_CONFIG_SECRET` | _(нет)_ | Секрет для прямой ссылки без входа в панель: `GET /api/clients/export-config?token=СЕКРЕТ&clientId=…&profileId=…` (при нескольких инстансах `profileId` обязателен). Не светите URL посторонним | | `ADMIN_PASSWORD` | _(генерируется)_ | Первый пароль вместо файла | | `SKIP_DOWNLOAD` | `0` | `1` — не качать GitHub, собрать из `INSTALL_DIR` | | `ALLOW_DEFAULT_PASSWORD` | `0` | `1` — см. раздел «Пароль» ниже | @@ -68,6 +69,8 @@ cd /opt/amnezia-admin && chmod +x scripts/install.sh && sudo SKIP_DOWNLOAD=1 bas | `LANDING_CONTAINER` | `amnezia-web-landing` | Имя контейнера лендинга | | `NO_CACHE` | `0` | `1` — `docker build --no-cache` при проблемах с обновлением образа | +Переменная **`AWG_PROFILES`** при установке автоматически сохраняется в **`/root/amnezia-admin.awg-profiles.json`** на VPS; при следующем запуске `install.sh` без `AWG_PROFILES` значение подставляется из этого файла или из **старого контейнера** `amnezia-admin` перед его удалением — так переключатель «Инстанс» не пропадает после обновления панели. + #### Несколько инстансов (AmneziaWG + Legacy и т.д.) Пути и имена контейнеров на сервере могут отличаться — проверьте внутри контейнера (`docker exec … ls /opt/amnezia`). Пример **двух** профилей при запуске установщика (одна строка JSON в кавычках): @@ -104,7 +107,13 @@ chmod +x scripts/warp-amnezia.sh ### Экспорт конфигурации клиента (.conf) -Если в записи клиента на сервере есть **`userData.last_config`** (JSON из приложения Amnezia с полем **`config`** — готовый текст — или с **`client_priv_key`** и ключами сервера), в таблице появится кнопка **«Скачать .conf»**. Такое содержимое сервер получает, когда клиент создавался/синхронизировался через официальное приложение; если на VPS только «голый» `clientsTable` без `last_config`, кнопки не будет — конфиг нужно брать из приложения на устройстве. +Если в записи клиента на сервере есть **`userData.last_config`** (JSON из приложения Amnezia с полем **`config`** — готовый текст — или с **`client_priv_key`** и ключами сервера), в таблице появятся **«Скачать .conf»**, **«Прямая ссылка»** и **«Копировать URL»**. Прямая ссылка имеет вид +`/api/clients/export-config?clientId=…&profileId=…` — последний параметр нужен только при нескольких инстансах (`AWG_PROFILES`). Работает в браузере, где вы уже вошли в панель (cookie-сессия). + +Чтобы скачивать **без входа в панель**, задайте **`EXPORT_CONFIG_SECRET`** при установке и открывайте +`/api/clients/export-config?token=ВАШ_СЕКРЕТ&clientId=…` — при **нескольких** профилях добавьте **`&profileId=…`**. Не пересылайте такую ссылку третьим лицам. + +Если на VPS только «голый» `clientsTable` без `last_config`, экспорта не будет — конфиг нужно брать из приложения Amnezia на устройстве. Для корректного **`Endpoint`** задайте **`CLIENT_CONFIG_ENDPOINT`** при установке (публичный IP или домен VPS), либо убедитесь, что в `last_config` указан **`hostName`**, либо открывайте панель по тому же хосту, который клиенты должны использовать для подключения (не `localhost`). diff --git a/public/app.js b/public/app.js index 141ffdf..bef3b06 100644 --- a/public/app.js +++ b/public/app.js @@ -512,6 +512,13 @@ function renderRows(clients) { btn("Переименовать", "btn small ghost", () => void renameClient(c)) ); nameWrap.append(strong, renameWrap); + if (!c.exportAvailable) { + const exHint = document.createElement("p"); + exHint.className = "muted export-missing-hint"; + exHint.textContent = + "Экспорт .conf с сервера недоступен: нет userData.last_config (ключи только в приложении Amnezia)."; + nameWrap.appendChild(exHint); + } nameTd.appendChild(nameWrap); const ipTd = document.createElement("td"); @@ -549,8 +556,21 @@ function renderRows(clients) { ); } if (c.exportAvailable) { + const direct = document.createElement("a"); + direct.className = "btn small ghost"; + direct.href = clientExportGetUrl(c.clientId); + direct.textContent = "Прямая ссылка"; + direct.rel = "noopener"; + direct.title = + "Открыть в новой вкладке — скачается .conf, если вы авторизованы в этой панели (cookies)."; + + actTd.appendChild(btn("Скачать .conf", "btn small ghost", () => void downloadClientConfig(c))); + actTd.appendChild(direct); actTd.appendChild( - btn("Скачать .conf", "btn small ghost", () => void downloadClientConfig(c)), + btn("Копировать URL", "btn small ghost", async () => { + const ok = await copyTextToClipboard(clientExportGetUrl(c.clientId)); + setStatus(ok ? "Ссылка скопирована (вставьте в браузер, будучи залогиненным)." : "Не удалось скопировать.", !ok); + }), ); } actTd.appendChild(btn("Удалить", "btn small warn", () => confirmDelete(c.name, c.clientId))); @@ -708,14 +728,46 @@ function escapeHtml(s) { .replace(/"/g, """); } +/** Query для нужного инстанса при нескольких профилях AWG_PROFILES */ +function currentProfileQuerySuffix() { + if (!protoSelect || !protoSwitch || protoSwitch.classList.contains("hidden")) return ""; + const pid = String(protoSelect.value || "").trim(); + return pid ? `&profileId=${encodeURIComponent(pid)}` : ""; +} + +/** Прямая GET-ссылка на скачивание (работает в браузере с активной сессией панели). */ +function clientExportGetUrl(clientId) { + const q = `clientId=${encodeURIComponent(clientId)}${currentProfileQuerySuffix()}`; + return `${window.location.origin}/api/clients/export-config?${q}`; +} + +async function copyTextToClipboard(text) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + try { + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.left = "-9999px"; + document.body.appendChild(ta); + ta.select(); + document.execCommand("copy"); + ta.remove(); + return true; + } catch { + return false; + } + } +} + async function downloadClientConfig(c) { try { setStatus("Готовлю конфиг…", false); - const res = await fetch("/api/clients/export-config", { - method: "POST", + const res = await fetch(clientExportGetUrl(c.clientId), { + method: "GET", credentials: "same-origin", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ clientId: c.clientId }), }); const text = await res.text(); if (!res.ok) { diff --git a/public/index.html b/public/index.html index 8ceb67f..7eda1a2 100644 --- a/public/index.html +++ b/public/index.html @@ -29,7 +29,7 @@
Панель сервера
Дату отключения задаёте сами (локальное время браузера). Кнопка «Выключить» убирает клиента из туннеля сразу. «Задать дату» без галочки — только запись в таблице; с галочкой — клиент остаётся в туннеле до наступления времени. Несколько инстансов AmneziaWG (Legacy и др.) — в переменной AWG_PROFILES и списке «Инстанс».
+Дату отключения задаёте сами (локальное время браузера). Кнопка «Выключить» убирает клиента из туннеля сразу. «Задать дату» без галочки — только запись в таблице; с галочкой — клиент остаётся в туннеле до наступления времени. Несколько инстансов задаются переменной AWG_PROFILES; установщик при апдейте подставляет её из прежнего контейнера или из /root/amnezia-admin.awg-profiles.json. Конфиг .conf — только если на сервере есть last_config; иначе используйте приложение Amnezia.