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.
This commit is contained in:
@@ -129,7 +129,11 @@ app.post("/ui/masterkeys", (q, r) => {
|
|||||||
app.patch("/ui/masterkeys/:id", (q, r) => {
|
app.patch("/ui/masterkeys/:id", (q, r) => {
|
||||||
const m = find(q.params.id); if (!m) return r.status(404).json({ error: "Не найден" });
|
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.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/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 }); });
|
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 }); });
|
||||||
|
|||||||
@@ -103,6 +103,25 @@ export default function MasterKeysPage({ token, showMsg }: PageProps) {
|
|||||||
catch (e) { showMsg(errText(e)); }
|
catch (e) { showMsg(errText(e)); }
|
||||||
}, [fetchLink, showMsg]);
|
}, [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 fold = (id: number) => setFolded(prev => {
|
||||||
const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n;
|
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) {
|
|||||||
<div className="mk-setting-ctl">
|
<div className="mk-setting-ctl">
|
||||||
<input id={`mk-limit-${k.id}`} className="field mk-limit-input" type="number" min={1} max={100}
|
<input id={`mk-limit-${k.id}`} className="field mk-limit-input" type="number" min={1} max={100}
|
||||||
value={limitEdit} onChange={e => setLimitEdit(e.target.value)} />
|
value={limitEdit} onChange={e => setLimitEdit(e.target.value)} />
|
||||||
<button className="btn btn--tonal" onClick={() =>
|
<button className="btn btn--tonal" onClick={() => saveLimit(k)}>
|
||||||
act(() => apiFetch('PATCH', `/ui/masterkeys/${k.id}`, token, { device_limit: Number(limitEdit) }), 'Лимит сохранён')}>
|
|
||||||
Сохранить
|
Сохранить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+12
-1
@@ -781,8 +781,19 @@ export function createSub(deps: { uidb: Database.Database; servers: Servers; bas
|
|||||||
ids = next;
|
ids = next;
|
||||||
}
|
}
|
||||||
q.masterUpdate.run(nl as string, nd as number, m.id);
|
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);
|
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.
|
// Новая ссылка; устройства продолжают работать: у них auth_pub, а не secret.
|
||||||
|
|||||||
Reference in New Issue
Block a user