mirror of
https://github.com/andrey271192/amnezia_web-PRO.git
synced 2026-09-20 14:42:00 +00:00
feat: sync app-created peers into clients table
This commit is contained in:
@@ -38,6 +38,8 @@
|
|||||||
|
|
||||||
Если клиенты есть в `awg0.conf`, но не отображаются из-за отсутствия/нестандартного `clientsTable`, обновите до **v1.2.35**. Таблица **«Пользователи»** теперь показывает объединение `clientsTable + [Peer]` из серверного конфига. Такие строки помечаются подсказкой: для экспорта `.conf` нужно импортировать клиентский `.conf` через блок **«Импорт клиента»**.
|
Если клиенты есть в `awg0.conf`, но не отображаются из-за отсутствия/нестандартного `clientsTable`, обновите до **v1.2.35**. Таблица **«Пользователи»** теперь показывает объединение `clientsTable + [Peer]` из серверного конфига. Такие строки помечаются подсказкой: для экспорта `.conf` нужно импортировать клиентский `.conf` через блок **«Импорт клиента»**.
|
||||||
|
|
||||||
|
Начиная с **v1.2.36** в блоке **«Пользователи»** есть кнопка **«Подтянуть из awg0.conf»**. Она создаёт недостающие строки `clientsTable` для уже активных `[Peer]`, которые были созданы в приложении Amnezia. VPN-конфиг и контейнер не пересоздаются. Экспорт `.conf` для таких пользователей появится только после импорта их клиентского `.conf`, потому приватный ключ клиента хранится в приложении, а не на сервере.
|
||||||
|
|
||||||
## Лендинг не поднимается (порт 80 занят)
|
## Лендинг не поднимается (порт 80 занят)
|
||||||
|
|
||||||
При ошибке bind `:80` используйте при установке **`SKIP_LANDING=1`** или **`LANDING_PORT=8081`** — админка на `HOST_PORT` (например 8080) от этого не зависит.
|
При ошибке bind `:80` используйте при установке **`SKIP_LANDING=1`** или **`LANDING_PORT=8081`** — админка на `HOST_PORT` (например 8080) от этого не зависит.
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "amnezia-admin",
|
"name": "amnezia-admin",
|
||||||
"version": "1.2.35",
|
"version": "1.2.36",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "amnezia-admin",
|
"name": "amnezia-admin",
|
||||||
"version": "1.2.35",
|
"version": "1.2.36",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.21.2"
|
"express": "^4.21.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "amnezia-admin",
|
"name": "amnezia-admin",
|
||||||
"version": "1.2.35",
|
"version": "1.2.36",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "amnezia_web-PRO — веб-панель AmneziaWG: несколько инстансов, WARP, каскад/Endpoint, экспорт .conf (Docker)",
|
"description": "amnezia_web-PRO — веб-панель AmneziaWG: несколько инстансов, WARP, каскад/Endpoint, экспорт .conf (Docker)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const rowsEl = document.querySelector("#rows");
|
|||||||
const statusEl = document.querySelector("#status");
|
const statusEl = document.querySelector("#status");
|
||||||
const peerCountEl = document.querySelector("#peer-count");
|
const peerCountEl = document.querySelector("#peer-count");
|
||||||
const wgShowEl = document.querySelector("#wg-show");
|
const wgShowEl = document.querySelector("#wg-show");
|
||||||
|
const syncPeersBtn = document.querySelector("#sync-peers-btn");
|
||||||
|
|
||||||
const warpPanel = document.querySelector("#warp-panel");
|
const warpPanel = document.querySelector("#warp-panel");
|
||||||
const warpStatusLine = document.querySelector("#warp-status-line");
|
const warpStatusLine = document.querySelector("#warp-status-line");
|
||||||
@@ -663,6 +664,10 @@ if (importForm) {
|
|||||||
importForm.addEventListener("submit", (ev) => void importClientConfig(ev));
|
importForm.addEventListener("submit", (ev) => void importClientConfig(ev));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (syncPeersBtn) {
|
||||||
|
syncPeersBtn.addEventListener("click", () => void syncPeersFromConfig());
|
||||||
|
}
|
||||||
|
|
||||||
const instanceForm = document.querySelector("#instance-form");
|
const instanceForm = document.querySelector("#instance-form");
|
||||||
if (instanceForm) {
|
if (instanceForm) {
|
||||||
instanceForm.addEventListener("submit", (ev) => void createInstance(ev));
|
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) {
|
async function confirmDelete(name, clientId) {
|
||||||
const ok = confirm(
|
const ok = confirm(
|
||||||
`Удалить клиента «${name}»? Конфиг из приложения Amnezia перестанет совпадать с сервером.`
|
`Удалить клиента «${name}»? Конфиг из приложения Amnezia перестанет совпадать с сервером.`
|
||||||
@@ -1627,7 +1655,15 @@ async function loadClients() {
|
|||||||
if (uiHidden.users) {
|
if (uiHidden.users) {
|
||||||
peerCountEl.textContent = "";
|
peerCountEl.textContent = "";
|
||||||
wgShowEl.textContent = "";
|
wgShowEl.textContent = "";
|
||||||
|
if (syncPeersBtn) syncPeersBtn.hidden = true;
|
||||||
} else {
|
} 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`;
|
peerCountEl.textContent = `${pref}${data.clients.length} в таблице · ${data.peerCount} peer`;
|
||||||
wgShowEl.textContent = data.wgShow || "";
|
wgShowEl.textContent = data.wgShow || "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -323,6 +323,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="panel-fold-body">
|
<div class="panel-fold-body">
|
||||||
|
<div class="users-tools">
|
||||||
|
<button id="sync-peers-btn" class="btn small ghost" type="button">Подтянуть из awg0.conf</button>
|
||||||
|
<span class="muted users-tools-hint">Для клиентов, созданных в приложении Amnezia</span>
|
||||||
|
</div>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="clients-table">
|
<table class="clients-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -625,6 +625,18 @@ h1 {
|
|||||||
font-size: 0.88rem;
|
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.clients-table {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|||||||
45
server.js
45
server.js
@@ -976,6 +976,11 @@ function clientDisplayNameFromRow(row, id) {
|
|||||||
return String(ud.clientName || row?.name || row?.clientName || `${id.slice(0, 10)}…`);
|
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). */
|
/** Совпадает с defaults Amnezia Desktop (protocolConstants awg, desktop MTU). */
|
||||||
const AWG_EXPORT_DEFAULTS = {
|
const AWG_EXPORT_DEFAULTS = {
|
||||||
Jc: "3",
|
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) {
|
async function serveClientConfigExport(req, res) {
|
||||||
if (IS_COMMUNITY) {
|
if (IS_COMMUNITY) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
|
|||||||
Reference in New Issue
Block a user