diff --git a/docs/panel-guide.md b/docs/panel-guide.md index e20ceb9..eef0316 100644 --- a/docs/panel-guide.md +++ b/docs/panel-guide.md @@ -38,6 +38,8 @@ Если клиенты есть в `awg0.conf`, но не отображаются из-за отсутствия/нестандартного `clientsTable`, обновите до **v1.2.35**. Таблица **«Пользователи»** теперь показывает объединение `clientsTable + [Peer]` из серверного конфига. Такие строки помечаются подсказкой: для экспорта `.conf` нужно импортировать клиентский `.conf` через блок **«Импорт клиента»**. +Начиная с **v1.2.36** в блоке **«Пользователи»** есть кнопка **«Подтянуть из awg0.conf»**. Она создаёт недостающие строки `clientsTable` для уже активных `[Peer]`, которые были созданы в приложении Amnezia. VPN-конфиг и контейнер не пересоздаются. Экспорт `.conf` для таких пользователей появится только после импорта их клиентского `.conf`, потому приватный ключ клиента хранится в приложении, а не на сервере. + ## Лендинг не поднимается (порт 80 занят) При ошибке bind `:80` используйте при установке **`SKIP_LANDING=1`** или **`LANDING_PORT=8081`** — админка на `HOST_PORT` (например 8080) от этого не зависит. diff --git a/package-lock.json b/package-lock.json index 01cb019..b17f03c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "amnezia-admin", - "version": "1.2.35", + "version": "1.2.36", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "amnezia-admin", - "version": "1.2.35", + "version": "1.2.36", "license": "MIT", "dependencies": { "express": "^4.21.2" diff --git a/package.json b/package.json index 88f0d17..c41145a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amnezia-admin", - "version": "1.2.35", + "version": "1.2.36", "private": false, "description": "amnezia_web-PRO — веб-панель AmneziaWG: несколько инстансов, WARP, каскад/Endpoint, экспорт .conf (Docker)", "license": "MIT", diff --git a/public/app.js b/public/app.js index 8efbbd6..15fd542 100644 --- a/public/app.js +++ b/public/app.js @@ -14,6 +14,7 @@ const rowsEl = document.querySelector("#rows"); const statusEl = document.querySelector("#status"); const peerCountEl = document.querySelector("#peer-count"); const wgShowEl = document.querySelector("#wg-show"); +const syncPeersBtn = document.querySelector("#sync-peers-btn"); const warpPanel = document.querySelector("#warp-panel"); const warpStatusLine = document.querySelector("#warp-status-line"); @@ -663,6 +664,10 @@ if (importForm) { importForm.addEventListener("submit", (ev) => void importClientConfig(ev)); } +if (syncPeersBtn) { + syncPeersBtn.addEventListener("click", () => void syncPeersFromConfig()); +} + const instanceForm = document.querySelector("#instance-form"); if (instanceForm) { instanceForm.addEventListener("submit", (ev) => void createInstance(ev)); @@ -1609,6 +1614,29 @@ async function mutate(path, clientId) { } } +async function syncPeersFromConfig() { + if (syncPeersBtn) syncPeersBtn.disabled = true; + try { + setStatus("Подтягиваю клиентов из awg0.conf…", false); + const data = await api("/api/clients/sync-peers", { + method: "POST", + body: JSON.stringify(withCurrentProfile({})), + }); + const added = Number(data.added || 0); + setStatus( + added + ? `Подтянуто клиентов: ${added}.` + : "Новых клиентов в awg0.conf нет — clientsTable уже совпадает.", + false, + ); + await loadClients(); + } catch (e) { + setStatus(String(e.message || e), true); + } finally { + if (syncPeersBtn) syncPeersBtn.disabled = false; + } +} + async function confirmDelete(name, clientId) { const ok = confirm( `Удалить клиента «${name}»? Конфиг из приложения Amnezia перестанет совпадать с сервером.` @@ -1627,7 +1655,15 @@ async function loadClients() { if (uiHidden.users) { peerCountEl.textContent = ""; wgShowEl.textContent = ""; + if (syncPeersBtn) syncPeersBtn.hidden = true; } else { + if (syncPeersBtn) { + syncPeersBtn.hidden = false; + syncPeersBtn.disabled = editionState.readOnlyClients; + syncPeersBtn.title = editionState.readOnlyClients + ? "Доступно в PRO" + : "Создать недостающие строки clientsTable из активных [Peer] текущего инстанса"; + } peerCountEl.textContent = `${pref}${data.clients.length} в таблице · ${data.peerCount} peer`; wgShowEl.textContent = data.wgShow || ""; } diff --git a/public/index.html b/public/index.html index fcccf0f..3c77ece 100644 --- a/public/index.html +++ b/public/index.html @@ -323,6 +323,10 @@
+
+ + Для клиентов, созданных в приложении Amnezia +
diff --git a/public/styles.css b/public/styles.css index f04d51e..1c519d1 100644 --- a/public/styles.css +++ b/public/styles.css @@ -625,6 +625,18 @@ h1 { font-size: 0.88rem; } +.users-tools { + display: flex; + align-items: center; + gap: 0.65rem; + flex-wrap: wrap; + margin: 0 0 0.75rem; +} + +.users-tools-hint { + font-size: 0.82rem; +} + table.clients-table { table-layout: fixed; } diff --git a/server.js b/server.js index 72413d6..f1a12fe 100644 --- a/server.js +++ b/server.js @@ -976,6 +976,11 @@ function clientDisplayNameFromRow(row, id) { return String(ud.clientName || row?.name || row?.clientName || `${id.slice(0, 10)}…`); } +function defaultPeerClientName(peer) { + const ip = String(peer?.allowedIPs || "").split(",")[0].trim().replace(/\/\d+$/, ""); + return ip ? `Клиент ${ip}` : `Peer ${String(peer?.publicKey || "").slice(0, 10)}…`; +} + /** Совпадает с defaults Amnezia Desktop (protocolConstants awg, desktop MTU). */ const AWG_EXPORT_DEFAULTS = { Jc: "3", @@ -2347,6 +2352,46 @@ app.get("/api/clients", requireAuth, async (req, res) => { } }); +app.post("/api/clients/sync-peers", requireAuth, requireProTier, async (req, res) => { + const rt = runtimeFromExportRequest(req); + try { + await rt.backupRemoteFiles(); + const { conf, clients } = await rt.loadState(); + const existing = new Set(clients.map(clientRowId).filter(Boolean)); + const now = new Date().toISOString(); + const added = []; + for (const peer of conf.peers) { + const id = peer.publicKey; + if (!id || existing.has(id)) continue; + const row = { + clientId: id, + userData: { + clientName: defaultPeerClientName(peer), + creationDate: now, + importedFromPeerAt: now, + allowedIps: peer.allowedIPs || "", + }, + }; + clients.push(row); + existing.add(id); + added.push({ clientId: id, name: row.userData.clientName, allowedIps: row.userData.allowedIps }); + } + if (added.length) { + await rt.dockerWriteFile(rt.clientsPath, stringifyClientsTable(clients)); + } + res.json({ + ok: true, + added: added.length, + totalPeers: conf.peers.length, + totalClientsTable: clients.length, + clients: added, + }); + } catch (e) { + console.error(e); + res.status(500).json({ error: String(e.message || e) }); + } +}); + async function serveClientConfigExport(req, res) { if (IS_COMMUNITY) { res.status(403).json({