diff --git a/README.md b/README.md index df714bd..841697e 100644 --- a/README.md +++ b/README.md @@ -104,8 +104,8 @@ curl -s -X POST http://HOST:PORT/api/v1/masterkeys \ | Метод | Что делает | |---|---| -| `POST /api/v1/masterkeys` | Создаёт ключ. Тело `{ label, device_limit?, servers? }` (лимит 1–100, по умолчанию 3; без `servers` — весь набор API-ключа). `201` с `{ id, label, device_limit, devices, servers, created_at, link, tls }`; `403` — сервер вне набора, `503` — подписка не настроена (ключ не создаётся) | -| `GET /api/v1/masterkeys` | Список: `{ keys: [{ id, label, device_limit, devices, servers, created_at, deleting? }] }` | +| `POST /api/v1/masterkeys` | Создаёт ключ. Тело `{ label, device_limit?, servers? }` (лимит 1–100, по умолчанию 3; без `servers` — весь набор API-ключа). `201` с `{ id, uuid, label, device_limit, devices, servers, created_at, link, tls }`; `403` — сервер вне набора, `503` — подписка не настроена (ключ не создаётся) | +| `GET /api/v1/masterkeys` | Список: `{ keys: [{ id, uuid, label, device_limit, devices, servers, created_at, deleting? }] }`. `uuid` — постоянный идентификатор ключа: не меняется ни при перевыпуске ссылки, ни при правке метки. Везде, где в пути `:id`, можно передать и `uuid` | | `GET /api/v1/masterkeys/:id` | Ключ, ссылка и устройства одним запросом: те же поля + `link`, `tls`, `device_list` (`devices` — счётчик) | | `PATCH /api/v1/masterkeys/:id` | `{ label?, device_limit?, servers? }` — смена серверов сразу добавляет/снимает пиров устройств | | `POST /api/v1/masterkeys/:id/rotate` | Новая ссылка `{ id, link, tls }`; старая не принимает новые устройства, подключённые работают дальше | @@ -196,7 +196,7 @@ base64url-декодировать, отбросить первые 4 байта | Метод | Что делает | |---|---| -| `GET /ui/masterkeys` | `{ enabled, tls, keys: [{ id, label, device_limit, devices, servers, created_at, deleting? }] }` | +| `GET /ui/masterkeys` | `{ enabled, tls, keys: [{ id, uuid, label, device_limit, devices, servers, created_at, deleting? }] }`; `:id` в путях — id или `uuid` | | `POST /ui/masterkeys` | `{ label, device_limit?, servers? }` → `201` | | `PATCH /ui/masterkeys/:id` | `{ label?, device_limit?, servers? }` | | `GET /ui/masterkeys/:id/link` | `{ link, tls }` — ссылка `sen://…` | diff --git a/awg-ui/mock-server.ts b/awg-ui/mock-server.ts index 1096998..2e1f5de 100644 --- a/awg-ui/mock-server.ts +++ b/awg-ui/mock-server.ts @@ -50,12 +50,12 @@ let apiId = 1; // ── мастер-ключи sen:// ── interface Dev { id: number; device_id: string; device_name: string; platform: string; version: string; created_at: number; last_seen: number | null; rekey_requested: boolean; online: boolean; lastHandshake: number; rx: number; tx: number } -interface MK { id: number; label: string; device_limit: number; created_at: number; secret: Buffer; devs: Dev[]; servers: number[]; deleting?: number[] } +interface MK { id: number; uuid: string; label: string; device_limit: number; created_at: number; secret: Buffer; devs: Dev[]; servers: number[]; deleting?: number[] } let mkId = 1, devId = 1; const dev = (name: string, platform: string, online: boolean, version = "0.6.5"): Dev => ({ id: devId++, device_id: crypto.randomUUID(), device_name: name, platform, version, created_at: now() - 86400, last_seen: now() - rnd(900), rekey_requested: false, online, lastHandshake: now() - rnd(300), rx: rnd(9e8), tx: rnd(3e8) }); -const masters: MK[] = [{ id: mkId++, label: "Семья", device_limit: 3, created_at: now() - 86400 * 2, secret: crypto.randomBytes(16), devs: [dev("MacBook Ивана", "macOS", true), dev("Windows-ПК", "Windows", false, "0.6.2")], servers: [0, 1] }]; +const masters: MK[] = [{ id: mkId++, uuid: crypto.randomUUID(), label: "Семья", device_limit: 3, created_at: now() - 86400 * 2, secret: crypto.randomBytes(16), devs: [dev("MacBook Ивана", "macOS", true), dev("Windows-ПК", "Windows", false, "0.6.2")], servers: [0, 1] }]; const signPub = crypto.randomBytes(32); -const mkOut = (m: MK) => ({ id: m.id, label: m.label, device_limit: m.device_limit, devices: m.devs.length, servers: m.servers, created_at: m.created_at, ...(m.deleting ? { deleting: m.deleting } : {}) }); +const mkOut = (m: MK) => ({ id: m.id, uuid: m.uuid, label: m.label, device_limit: m.device_limit, devices: m.devs.length, servers: m.servers, created_at: m.created_at, ...(m.deleting ? { deleting: m.deleting } : {}) }); // Мок: устройство «догнано» на всех серверах ключа, кроме Tokyo (id 2 — ещё не подключалась). const devOut = (m: MK, d: Dev) => ({ ...d, servers_total: m.servers.length, servers_ok: m.servers.filter(s => s !== 2).length }); const find = (id: string) => masters.find(m => m.id === Number(id) && !m.deleting); @@ -123,7 +123,7 @@ app.get("/ui/masterkeys", (_q, r) => r.json({ enabled: true, tls: false, keys: m app.post("/ui/masterkeys", (q, r) => { const limit = q.body.device_limit ?? 3; if (!q.body.label || !Number.isInteger(limit) || limit < 1 || limit > 100) return r.status(400).json({ error: "Неверные параметры" }); - const m: MK = { id: mkId++, label: q.body.label, device_limit: limit, created_at: now(), secret: crypto.randomBytes(16), devs: [], servers: q.body.servers ?? [0] }; + const m: MK = { id: mkId++, uuid: crypto.randomUUID(), label: q.body.label, device_limit: limit, created_at: now(), secret: crypto.randomBytes(16), devs: [], servers: q.body.servers ?? [0] }; masters.push(m); r.status(201).json(mkOut(m)); }); app.patch("/ui/masterkeys/:id", (q, r) => { diff --git a/awg-ui/src/tabs/masterkeys/index.tsx b/awg-ui/src/tabs/masterkeys/index.tsx index e81ba85..075cf9f 100644 --- a/awg-ui/src/tabs/masterkeys/index.tsx +++ b/awg-ui/src/tabs/masterkeys/index.tsx @@ -12,7 +12,7 @@ import './masterkeys.css'; // плюс лимит устройств; устройства регистрируются сами (приватный ключ остаётся // на них) и здесь только отображаются и отзываются. Формат — docs/sen-link.md. // deleting — ключ удалён и уже не работает, но эти серверы ещё не подтвердили, что сняли его пиров. -interface MasterKey { id: number; label: string; device_limit: number; devices: number; servers: number[]; created_at: number; deleting?: number[] } +interface MasterKey { id: number; uuid: string; label: string; device_limit: number; devices: number; servers: number[]; created_at: number; deleting?: number[] } interface Device { id: number; device_id: string; device_name: string; platform: string; version: string; created_at: number; last_seen: number | null; rekey_requested: boolean; online: boolean; lastHandshake: number; rx: number; tx: number; @@ -138,6 +138,19 @@ export default function MasterKeysPage({ token, showMsg }: PageProps) { {/* Слева — настройки самого ключа */}

Настройки ключа

+
+
+ UUID + {k.uuid} + Постоянный: перевыпуск ссылки его не меняет +
+
+ +
+
@@ -221,6 +234,17 @@ export default function MasterKeysPage({ token, showMsg }: PageProps) {
); + // UUID ключа в строке списка — как ID устройств под ним (та же ширина и обрезка), + // полностью — в подсказке, по клику копируется. + const uuidCell = (k: MasterKey) => ( + + + + ); + const caret = (k: MasterKey) => (