fix: show peers missing clients table

This commit is contained in:
andrey271192
2026-06-21 19:16:01 +03:00
parent cb5fca3d0f
commit fd66b0a024
5 changed files with 95 additions and 34 deletions

View File

@@ -36,6 +36,8 @@
Если переключатель **«Инстанс»** показывает правильный контейнер, но таблица **«Пользователи»** остаётся от другого инстанса, обновите до **v1.2.34**. Клиентские запросы (`/api/clients` и операции над пользователями) теперь всегда передают явный `profileId`, а не полагаются только на cookie выбранного профиля.
Если клиенты есть в `awg0.conf`, но не отображаются из-за отсутствия/нестандартного `clientsTable`, обновите до **v1.2.35**. Таблица **«Пользователи»** теперь показывает объединение `clientsTable + [Peer]` из серверного конфига. Такие строки помечаются подсказкой: для экспорта `.conf` нужно импортировать клиентский `.conf` через блок **«Импорт клиента»**.
## Лендинг не поднимается (порт 80 занят)
При ошибке bind `:80` используйте при установке **`SKIP_LANDING=1`** или **`LANDING_PORT=8081`** — админка на `HOST_PORT` (например 8080) от этого не зависит.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "amnezia-admin",
"version": "1.2.34",
"version": "1.2.35",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "amnezia-admin",
"version": "1.2.34",
"version": "1.2.35",
"license": "MIT",
"dependencies": {
"express": "^4.21.2"

View File

@@ -1,6 +1,6 @@
{
"name": "amnezia-admin",
"version": "1.2.34",
"version": "1.2.35",
"private": false,
"description": "amnezia_web-PRO — веб-панель AmneziaWG: несколько инстансов, WARP, каскад/Endpoint, экспорт .conf (Docker)",
"license": "MIT",

View File

@@ -942,12 +942,15 @@ function renderRows(clients) {
const hintFold = document.createElement("details");
hintFold.className = "hint-mini";
const sum = document.createElement("summary");
sum.textContent = "Нет готового .conf на сервере (last_config)";
sum.textContent = c.missingClientTable
? "Peer найден в конфиге, но нет записи clientsTable"
: "Нет готового .conf на сервере (last_config)";
hintFold.appendChild(sum);
const exHint = document.createElement("p");
exHint.className = "muted export-missing-hint";
exHint.textContent =
"Конфиг с сервера недоступен (нет last_config). Создайте клиента с нужным Endpoint в блоке «Новый клиент под каскад» ниже или возьмите ключ из приложения Amnezia.";
exHint.textContent = c.missingClientTable
? "Клиент активен в awg0.conf, но веб не знает имя и last_config. Вставьте его .conf в блок «Импорт клиента», чтобы восстановить запись и экспорт."
: "Конфиг с сервера недоступен (нет last_config). Создайте клиента с нужным Endpoint в блоке «Новый клиент под каскад» ниже или возьмите ключ из приложения Amnezia.";
hintFold.appendChild(exHint);
nameWrap.appendChild(hintFold);
}

110
server.js
View File

@@ -963,6 +963,19 @@ function stringifyClientsTable(rows) {
return `${JSON.stringify(rows, null, 4)}\n`;
}
function clientRowId(row) {
return String(row?.clientId ?? row?.id ?? row?.publicKey ?? row?.client_id ?? "").trim();
}
function clientRowUserData(row) {
return row?.userData && typeof row.userData === "object" ? row.userData : {};
}
function clientDisplayNameFromRow(row, id) {
const ud = clientRowUserData(row);
return String(ud.clientName || row?.name || row?.clientName || `${id.slice(0, 10)}`);
}
/** Совпадает с defaults Amnezia Desktop (protocolConstants awg, desktop MTU). */
const AWG_EXPORT_DEFAULTS = {
Jc: "3",
@@ -1173,7 +1186,7 @@ ${pskLine}AllowedIPs = ${allowedIps.join(", ")}
);
}
}
const existingClientIdx = clients.findIndex((c) => c.clientId === pub);
const existingClientIdx = clients.findIndex((c) => clientRowId(c) === pub);
const last_config = JSON.stringify(buildImportedLastConfig(normalized, sections, allowedIps));
const now = new Date().toISOString();
const name = normalizeImportName(clientName, `Импорт ${allowedIps[0].replace(/\/\d+$/, "")}`);
@@ -1292,7 +1305,7 @@ async function buildClientConfExport(rt, lc, ifaceMap, req, row, conf) {
);
}
const peer = conf.peers.find((p) => p.publicKey === row.clientId);
const peer = conf.peers.find((p) => p.publicKey === clientRowId(row));
const tunnelIp = tunnelClientIpv4(peer || {}, lc, row);
let serverPub = pickLc(lc, "server_pub_key", "serverPubKey");
@@ -1513,9 +1526,13 @@ async function disableClient(rt, clientId, ts) {
}
const nextPeers = conf.peers.filter((p) => p.publicKey !== clientId);
const nextConfText = serializeAwgConf(conf.head, nextPeers);
const idx = clients.findIndex((c) => c.clientId === clientId);
if (idx === -1) throw new Error("Client not in clientsTable");
const ud = { ...(clients[idx].userData || {}) };
let idx = clients.findIndex((c) => clientRowId(c) === clientId);
const peerRow = { clientId, userData: {} };
if (idx === -1) {
clients.push(peerRow);
idx = clients.length - 1;
}
const ud = { ...clientRowUserData(clients[idx]) };
ud.disabled = true;
ud.disabledAt = ts;
ud.lastDisconnectedAt = ts;
@@ -1535,10 +1552,11 @@ async function processScheduledDisconnects(rt) {
for (const c of clients) {
const ud = c.userData || {};
const iso = ud.scheduledTunnelDisconnectAt;
if (!iso || !peerByKey.get(c.clientId)) continue;
const clientId = clientRowId(c);
if (!iso || !peerByKey.get(clientId)) continue;
const t = new Date(iso).getTime();
if (Number.isNaN(t) || t > now) continue;
due.push({ clientId: c.clientId, ts: new Date(iso).toISOString() });
due.push({ clientId, ts: new Date(iso).toISOString() });
}
if (!due.length) return;
await rt.backupRemoteFiles();
@@ -2242,14 +2260,18 @@ app.get("/api/clients", requireAuth, async (req, res) => {
warpMeta.supported && warpMeta.installed ? warpMeta.selectedAllowedIps : [],
);
const { conf, clients, peerByKey } = await rt.loadState();
const rows = clients.map((c) => {
const id = c.clientId;
const rows = [];
const seenClientIds = new Set();
for (const c of clients) {
const id = clientRowId(c);
if (!id) continue;
seenClientIds.add(id);
const peer = peerByKey.get(id);
const ud = c.userData || {};
const ud = clientRowUserData(c);
const activeInConf = !!peer;
return {
rows.push({
clientId: id,
name: ud.clientName || `${id.slice(0, 10)}`,
name: clientDisplayNameFromRow(c, id),
allowedIps: peer?.allowedIPs || ud.allowedIps || ud.preservedAllowedIPs || null,
activeInConf,
disabled: !activeInConf,
@@ -2265,8 +2287,33 @@ app.get("/api/clients", requireAuth, async (req, res) => {
activeInConf &&
peerUsesWarp(peer, warpSelected),
exportAvailable: clientHasExportableLastConfig(c),
};
source: "clientsTable",
});
}
for (const peer of conf.peers) {
const id = peer.publicKey;
if (!id || seenClientIds.has(id)) continue;
rows.push({
clientId: id,
name: `Peer ${id.slice(0, 10)}`,
allowedIps: peer.allowedIPs || null,
activeInConf: true,
disabled: false,
disabledAt: null,
lastDisconnectedAt: null,
scheduledTunnelDisconnectAt: null,
creationDate: null,
latestHandshake: null,
dataReceived: null,
dataSent: null,
warpEnabled:
Boolean(warpMeta.supported && warpMeta.installed) &&
peerUsesWarp(peer, warpSelected),
exportAvailable: false,
source: "peer",
missingClientTable: true,
});
}
const warpOut =
warpMeta.supported === false
? { supported: false }
@@ -2344,7 +2391,7 @@ async function serveClientConfigExport(req, res) {
}
try {
const { conf, clients } = await rt.loadState();
const row = clients.find((c) => c.clientId === clientId);
const row = clients.find((c) => clientRowId(c) === clientId);
if (!row) {
res.status(404).json({ error: "Клиент не найден в clientsTable" });
return;
@@ -2421,7 +2468,7 @@ app.post("/api/clients/create", requireAuth, requireProTier, async (req, res) =>
}
const serverPub = await wgPubkeyFromPrivate(rt, ifaceMap.PrivateKey);
const { priv, pub } = await awgGenKeypair(rt);
if (clients.some((c) => c.clientId === pub)) {
if (clients.some((c) => clientRowId(c) === pub)) {
res.status(409).json({ error: "Коллизия ключей — попробуйте ещё раз." });
return;
}
@@ -2530,7 +2577,7 @@ app.post("/api/clients/create-cascade", requireAuth, requireProTier, async (req,
const serverPub = await wgPubkeyFromPrivate(rt, ifaceMap.PrivateKey);
const { priv, pub } = await awgGenKeypair(rt);
if (clients.some((c) => c.clientId === pub)) {
if (clients.some((c) => clientRowId(c) === pub)) {
res.status(409).json({ error: "Коллизия ключей — попробуйте ещё раз." });
return;
}
@@ -2734,7 +2781,7 @@ app.post("/api/clients/enable", requireAuth, requireProTier, async (req, res) =>
if (existing) {
return res.status(409).json({ error: "Peer already enabled" });
}
const idx = clients.findIndex((c) => c.clientId === clientId);
const idx = clients.findIndex((c) => clientRowId(c) === clientId);
if (idx === -1) {
return res.status(404).json({ error: "Client not in clientsTable" });
}
@@ -2786,10 +2833,14 @@ app.post("/api/clients/disconnect-date", requireAuth, requireProTier, async (req
const scheduleTunnelDisconnect = Boolean(req.body?.scheduleTunnelDisconnect);
try {
const { clients, peerByKey } = await rt.loadState();
const idx = clients.findIndex((c) => c.clientId === clientId);
if (idx === -1) return res.status(404).json({ error: "Client not in clientsTable" });
let idx = clients.findIndex((c) => clientRowId(c) === clientId);
const peer = peerByKey.get(clientId);
const ud = { ...(clients[idx].userData || {}) };
if (idx === -1) {
if (!peer) return res.status(404).json({ error: "Client not in clientsTable" });
clients.push({ clientId, userData: { allowedIps: peer.allowedIPs || "" } });
idx = clients.length - 1;
}
const ud = { ...clientRowUserData(clients[idx]) };
if (scheduleTunnelDisconnect) {
if (!peer) {
return res.status(400).json({
@@ -2827,10 +2878,15 @@ app.post("/api/clients/rename", requireAuth, requireProTier, async (req, res) =>
return res.status(400).json({ error: "Имя не длиннее 200 символов" });
}
try {
const { clients } = await rt.loadState();
const idx = clients.findIndex((c) => c.clientId === clientId);
if (idx === -1) return res.status(404).json({ error: "Client not in clientsTable" });
const ud = { ...(clients[idx].userData || {}), clientName: name };
const { clients, peerByKey } = await rt.loadState();
let idx = clients.findIndex((c) => clientRowId(c) === clientId);
if (idx === -1) {
const peer = peerByKey.get(clientId);
if (!peer) return res.status(404).json({ error: "Client not in clientsTable" });
clients.push({ clientId, userData: { allowedIps: peer.allowedIPs || "" } });
idx = clients.length - 1;
}
const ud = { ...clientRowUserData(clients[idx]), clientName: name };
clients[idx] = { ...clients[idx], userData: ud };
await rt.dockerWriteFile(rt.clientsPath, stringifyClientsTable(clients));
res.json({ ok: true });
@@ -2848,9 +2904,9 @@ app.post("/api/clients/delete", requireAuth, requireProTier, async (req, res) =>
await rt.backupRemoteFiles();
const { conf, clients } = await rt.loadState();
const nextPeers = conf.peers.filter((p) => p.publicKey !== clientId);
const nextClients = clients.filter((c) => c.clientId !== clientId);
if (nextClients.length === clients.length) {
return res.status(404).json({ error: "Client not in clientsTable" });
const nextClients = clients.filter((c) => clientRowId(c) !== clientId);
if (nextClients.length === clients.length && nextPeers.length === conf.peers.length) {
return res.status(404).json({ error: "Client not found" });
}
const nextConfText = serializeAwgConf(conf.head, nextPeers);
await rt.dockerWriteFile(rt.confPath, nextConfText);