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:
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2026 Ivan Vasilev
|
||||
// This source code is licensed under the MIT license found in the
|
||||
// LICENSE file in the root directory of this source tree.
|
||||
import { type NodeInfo, genLabel } from '../lib/shared';
|
||||
|
||||
// Выбор набора серверов галочками: серверы мастер-ключа, видимость API-ключа. Набор не
|
||||
// бывает пустым — последний отмеченный сервер снять нельзя. onChange получает новый набор
|
||||
// и сервер, который переключили.
|
||||
export default function ServerPicker({ nodes, selected, onChange }: {
|
||||
nodes: NodeInfo[];
|
||||
selected: number[];
|
||||
onChange: (next: number[], node: NodeInfo, added: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{nodes.map(n => {
|
||||
const on = selected.includes(n.id);
|
||||
const last = on && selected.length === 1;
|
||||
const h = n.health;
|
||||
return (
|
||||
<label className={`server-pick${last ? ' server-pick--locked' : ''}`} key={n.id}>
|
||||
<input type="checkbox" checked={on} disabled={last}
|
||||
onChange={() => onChange(on ? selected.filter(id => id !== n.id) : [...selected, n.id], n, !on)} />
|
||||
<span className={`chip chip--${n.online ? 'online' : 'offline'} server-pick-dot`} title={n.online ? 'В сети' : 'Нет связи'}>
|
||||
<span className="chip-dot" />
|
||||
</span>
|
||||
<span className="server-pick-name">{h?.server || n.name}</span>
|
||||
{h?.ip && <code className="server-pick-ip">{h.ip}</code>}
|
||||
{h?.gen && <span className="server-pick-gen">{genLabel(h.gen)}</span>}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Имя сервера для подписей: как в сервер-баре, иначе «#id».
|
||||
export const serverName = (nodes: NodeInfo[], id: number): string => {
|
||||
const n = nodes.find(x => x.id === id);
|
||||
return n ? n.health?.server || n.name : `#${id}`;
|
||||
};
|
||||
Reference in New Issue
Block a user