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.
42 lines
2.1 KiB
TypeScript
42 lines
2.1 KiB
TypeScript
// 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}`;
|
||
};
|