From 565b2759edf3c7634eea1a101b13211364747a35 Mon Sep 17 00:00:00 2001 From: Ivan Vasilev Date: Mon, 28 Sep 2026 14:12:27 +0300 Subject: [PATCH] Unbind the newest devices when a master key's limit drops Lowering device_limit below the number of bound devices now forgets the extra ones right away, newest first, so the devices that took their slots first keep them. Their peers are removed and they get 410 like a revoke from the panel. PATCH returns the unbound ids; the panel names the devices that will be dropped and asks before saving. --- awg-ui/mock-server.ts | 6 +++++- awg-ui/src/tabs/masterkeys/index.tsx | 22 ++++++++++++++++++++-- awg-ui/sub.ts | 13 ++++++++++++- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/awg-ui/mock-server.ts b/awg-ui/mock-server.ts index 2e1f5de..cabd5c8 100644 --- a/awg-ui/mock-server.ts +++ b/awg-ui/mock-server.ts @@ -129,7 +129,11 @@ app.post("/ui/masterkeys", (q, r) => { app.patch("/ui/masterkeys/:id", (q, r) => { const m = find(q.params.id); if (!m) return r.status(404).json({ error: "Не найден" }); if (q.body.servers !== undefined) { if (!Array.isArray(q.body.servers) || !q.body.servers.length) return r.status(400).json({ error: "Нужен хотя бы один сервер" }); m.servers = [...q.body.servers].sort(); } - if (q.body.label) m.label = q.body.label; if (q.body.device_limit) m.device_limit = q.body.device_limit; r.json(mkOut(m)); + if (q.body.label) m.label = q.body.label; if (q.body.device_limit) m.device_limit = q.body.device_limit; + // Как sub.ts: лишние устройства сверх лимита — отвязать, начиная с подключившихся последними. + const unbound = [...m.devs].sort((a, b) => b.created_at - a.created_at || b.id - a.id).slice(0, Math.max(0, m.devs.length - m.device_limit)).map(d => d.id); + m.devs = m.devs.filter(d => !unbound.includes(d.id)); + r.json({ ...mkOut(m), unbound }); }); app.post("/ui/masterkeys/:id/rotate", (q, r) => { const m = find(q.params.id); if (!m) return r.status(404).json({ error: "Не найден" }); m.secret = crypto.randomBytes(16); r.json({ id: m.id }); }); app.post("/ui/masterkeys/:id/rekey", (q, r) => { const m = find(q.params.id); if (!m) return r.status(404).json({ error: "Не найден" }); m.devs.forEach(d => d.rekey_requested = true); r.json({ id: m.id }); }); diff --git a/awg-ui/src/tabs/masterkeys/index.tsx b/awg-ui/src/tabs/masterkeys/index.tsx index 075cf9f..66d1335 100644 --- a/awg-ui/src/tabs/masterkeys/index.tsx +++ b/awg-ui/src/tabs/masterkeys/index.tsx @@ -103,6 +103,25 @@ export default function MasterKeysPage({ token, showMsg }: PageProps) { catch (e) { showMsg(errText(e)); } }, [fetchLink, showMsg]); + // Лимит ниже числа устройств: сервер сразу отвяжет лишних, начиная с подключившихся последними, — + // поэтому сперва спросить и назвать, кого именно. + const saveLimit = useCallback(async (k: MasterKey) => { + const limit = Number(limitEdit); + const list = devs[k.id] ?? []; + const extra = list.length - limit; + if (Number.isInteger(limit) && extra > 0) { + const names = [...list].sort((a, b) => b.created_at - a.created_at || b.id - a.id).slice(0, extra) + .map(d => `«${d.device_name || d.id}»`).join(', '); + if (!confirm(`Лимит ${limit} меньше, чем устройств (${list.length}). Будут отвязаны подключившиеся последними: ${names}. Продолжить?`)) return; + } + try { + const r = await apiFetch('PATCH', `/ui/masterkeys/${k.id}`, token, { device_limit: limit }); + const n: number = r.unbound?.length ?? 0; + showMsg(n ? `Лимит сохранён, отвязано устройств: ${n}` : 'Лимит сохранён'); + await load(); + } catch (e) { showMsg(errText(e)); } + }, [limitEdit, devs, token, load, showMsg]); + const fold = (id: number) => setFolded(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; }); @@ -159,8 +178,7 @@ export default function MasterKeysPage({ token, showMsg }: PageProps) {
setLimitEdit(e.target.value)} /> -
diff --git a/awg-ui/sub.ts b/awg-ui/sub.ts index 910a878..4b70c54 100644 --- a/awg-ui/sub.ts +++ b/awg-ui/sub.ts @@ -781,8 +781,19 @@ export function createSub(deps: { uidb: Database.Database; servers: Servers; bas ids = next; } q.masterUpdate.run(nl as string, nd as number, m.id); + // Лимит стал меньше, чем устройств: лишние отвязываются, начиная с подключившихся последними, — + // первые, кто занял места, их сохраняют. Как отзыв в панели: пиры снимаются, устройство получит 410. + const unbound: number[] = []; + const devices = q.devicesOf.all(m.id) as DeviceRow[]; + if (devices.length > (nd as number)) { + const newest = [...devices].sort((a, b) => b.created_at - a.created_at || b.id - a.id); + for (const d of newest.slice(0, devices.length - (nd as number))) { + affected.push(...forgetDevice(d)); + unbound.push(d.id); + } + } await reconcileAll(affected); - return { id: m.id, label: nl as string, device_limit: nd as number, servers: ids }; + return { id: m.id, label: nl as string, device_limit: nd as number, servers: ids, unbound }; } // Новая ссылка; устройства продолжают работать: у них auth_pub, а не secret.