From 1278ad527d719ac5a1da33aedad3f2adcd2692bf Mon Sep 17 00:00:00 2001 From: Ivan Vasilev Date: Fri, 25 Sep 2026 17:21:57 +0300 Subject: [PATCH] Add /sub/v1/devices and /sub/v1/peek, count the slots on register - GET /sub/v1/devices: a device lists the devices of its own master key (name, platform, version, last seen, and which one is asking). Read-only: unbinding is still only DELETE /sub/v1/device for oneself. - POST /sub/v1/peek: how many slots of a key are taken, asked with the link's secret before any device exists, so the client can show it while the link is only pasted. Wrong secrets count against the failure limit. - register also answers with devices / device_limit, so the client needs no second request to show the slot it just took. --- awg-ui/sub.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/awg-ui/sub.ts b/awg-ui/sub.ts index 42802e3..f5f69b2 100644 --- a/awg-ui/sub.ts +++ b/awg-ui/sub.ts @@ -269,7 +269,27 @@ export function createSub(deps: { uidb: Database.Database; ctrl: Ctrl; baseDir: return; } const dev = q.deviceById.get(deviceRowId) as DeviceRow; - send(res, 201, { device: dev.id, config: await buildConfig(dev) }); + // devices / device_limit: сколько мест занято с этим устройством — клиент показывает это сразу, + // без отдельного запроса списка. + send(res, 201, { + device: dev.id, config: await buildConfig(dev), + devices: (q.deviceCount.get(master.id) as { n: number }).n, device_limit: master.device_limit, + }); + })); + + // Сколько мест у ключа занято — клиент показывает это, пока ссылку только вставили и ещё ничего не + // регистрировали. Запрос не подписан устройством (его ещё нет): доступ по секрету из ссылки, как у register, + // а ответ подписан, как всегда. Неверный секрет считается в failLimit — секрет не перебрать. + sub.post("/sub/v1/peek", wrap(async (req, res) => { + const ip = req.socket.remoteAddress ?? "unknown"; + const secretB = typeof (req.body as any)?.sub === "string" && /^[A-Za-z0-9_-]{22}$/.test((req.body as any).sub) ? (req.body as any).sub as string : null; + const master = secretB ? q.masterBySecret.get(Buffer.from(secretB, "base64url")) as MasterRow | undefined : undefined; + if (!master) { + if (!failLimit(ip)) { send(res, 429, { error: "rate_limited" }); return; } + send(res, secretB ? 404 : 400, { error: secretB ? "not_found" : "bad_request" }); + return; + } + send(res, 200, { devices: (q.deviceCount.get(master.id) as { n: number }).n, device_limit: master.device_limit }); })); sub.get("/sub/v1/config", deviceAuth, wrap(async (req, res) => {