Manage master keys through the external API
API keys now see a set of servers instead of one, and /api/v1 gains the master-key operations the panel already had. - api_key_servers holds each key's servers (migrated from server_id, which stays the default server for /api/v1/users). /users follows X-Server-Id within the set; a server outside it is 404. PATCH /ui/apikeys/:id changes the set without reissuing the key. - /api/v1/masterkeys: create (returns the sen:// link), list, get with link and devices, patch label/limit/servers, rotate, rekey, delete, and nested device routes (unlink, rekey, PSK). A key sees a master key only if all of its servers are in the key's set, so it cannot delete or re-point service on a server it cannot see; anything else is 404, servers outside the set are 403. Creating refuses with 503 before writing anything if the subscription cannot issue links. - sub.ts: the panel routes and the API share one service layer, the panel's behaviour is unchanged. - Node deletion: owners check in beforeDelete (no writes, a later hook may still veto) and clean up in afterDelete. Previously a forced delete could release a waiting master key and then be vetoed. A node that is the only server of an API key is refused; other keys just lose it. - Panel: server checkboxes are a shared ServerPicker, used by the master key settings and the new API key settings row; the API keys table shows each key's servers. - README: the API section covers server sets and master keys.
This commit is contained in:
+9
-10
@@ -148,12 +148,15 @@ export function createNodes(deps: { uidb: Database.Database; ctrlLocal: Ctrl; ba
|
||||
const onlineHandlers: ((id: number) => void)[] = [];
|
||||
const onOnline = (cb: (id: number) => void) => { onlineHandlers.push(cb); };
|
||||
|
||||
// Подписчики на «сервер удаляют»: подписка сперва снимает с него своих пиров и может
|
||||
// запретить удаление. forcible — запрет снимается подтверждением оператора (force):
|
||||
// сервер просто не всё успел подтвердить, а не используется.
|
||||
// Подписчики на «сервер удаляют». beforeDelete — проверка: может запретить удаление и не
|
||||
// меняет ui.db (запретить может и следующий подписчик). forcible — запрет снимается
|
||||
// подтверждением оператора (force): сервер не всё успел подтвердить, а не используется.
|
||||
// afterDelete — уборка своих ссылок на сервер, когда он уже удалён.
|
||||
type DeleteVeto = { error: string; forcible: boolean } | null;
|
||||
const deleteHandlers: ((id: number, force: boolean) => Promise<DeleteVeto>)[] = [];
|
||||
const beforeDelete = (cb: (id: number, force: boolean) => Promise<DeleteVeto>) => { deleteHandlers.push(cb); };
|
||||
const afterHandlers: ((id: number) => void)[] = [];
|
||||
const afterDelete = (cb: (id: number) => void) => { afterHandlers.push(cb); };
|
||||
|
||||
/** Сервер по умолчанию: локальный, а без него — первая нода. */
|
||||
function defaultId(): number {
|
||||
@@ -350,12 +353,7 @@ export function createNodes(deps: { uidb: Database.Database; ctrlLocal: Ctrl; ba
|
||||
const id = idOf(req);
|
||||
const n = validId(id) ? q.byId.get(id) as NodeRow | undefined : undefined;
|
||||
if (!n) { res.status(404).json({ error: "Не найден" }); return; }
|
||||
// Ключи API ссылаются на server_id — не оставляем их висеть в воздухе. Мастер-ключи
|
||||
// проверяет подписка в своём обработчике.
|
||||
let used = false;
|
||||
try { used = (uidb.prepare("SELECT COUNT(*) AS n FROM api_keys WHERE server_id = ?").get(id) as { n: number }).n > 0; }
|
||||
catch { /* таблицы ещё нет */ }
|
||||
if (used) { res.status(409).json({ error: "К серверу привязаны ключи API — сначала удалите их" }); return; }
|
||||
// Ключи API и мастер-ключи проверяют их владельцы в своих обработчиках (beforeDelete).
|
||||
const force = req.query.force === "1";
|
||||
for (const cb of deleteHandlers) {
|
||||
let veto: DeleteVeto;
|
||||
@@ -370,8 +368,9 @@ export function createNodes(deps: { uidb: Database.Database; ctrlLocal: Ctrl; ba
|
||||
for (const t of ["peer_tombstones", "server_profiles"]) {
|
||||
try { uidb.prepare(`DELETE FROM ${t} WHERE server_id = ?`).run(id); } catch { /* таблицы ещё нет */ }
|
||||
}
|
||||
for (const cb of afterHandlers) { try { cb(id); } catch (e) { console.warn(`nodes: уборка после удаления #${id}: ${(e as Error).message}`); } }
|
||||
res.json({ success: true, id });
|
||||
});
|
||||
|
||||
return { router, ctrlFor, exists, isOnline, ids, onOnline, beforeDelete, defaultId, start };
|
||||
return { router, ctrlFor, exists, isOnline, ids, onOnline, beforeDelete, afterDelete, defaultId, start };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user