From 0fe48124c945c4923eb10b8aafa0f5bc177e3b72 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 17 May 2026 10:15:01 +0200 Subject: [PATCH] refactor(api): move every client-shaped endpoint off /inbounds onto /clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 --- frontend/src/pages/api-docs/endpoints.js | 235 +++++-------- .../src/pages/clients/ClientInfoModal.vue | 2 +- frontend/src/pages/clients/ClientQrModal.vue | 2 +- frontend/src/pages/clients/useClients.js | 7 +- .../src/pages/inbounds/InboundInfoModal.vue | 4 +- frontend/src/pages/inbounds/InboundList.vue | 20 -- frontend/src/pages/inbounds/InboundsPage.vue | 42 --- frontend/src/pages/inbounds/useInbounds.js | 4 +- web/controller/client.go | 142 ++++++++ web/controller/inbound.go | 314 ------------------ web/service/client.go | 31 ++ 11 files changed, 268 insertions(+), 535 deletions(-) diff --git a/frontend/src/pages/api-docs/endpoints.js b/frontend/src/pages/api-docs/endpoints.js index 16966bb1..aa239bfc 100644 --- a/frontend/src/pages/api-docs/endpoints.js +++ b/frontend/src/pages/api-docs/endpoints.js @@ -88,24 +88,6 @@ export const sections = [ { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, ], }, - { - method: 'GET', - path: '/panel/api/inbounds/getClientTraffics/:email', - summary: 'Traffic counters for a client identified by email.', - params: [ - { name: 'email', in: 'path', type: 'string', desc: 'Client email (unique across the panel).' }, - ], - response: '{\n "success": true,\n "obj": {\n "email": "user1",\n "up": 1048576,\n "down": 2097152,\n "total": 10737418240,\n "expiryTime": 1735689600000\n }\n}', - }, - { - method: 'GET', - path: '/panel/api/inbounds/getClientTrafficsById/:id', - summary: 'Traffic counters for a client identified by its UUID/password.', - params: [ - { name: 'id', in: 'path', type: 'string', desc: 'Client subId / UUID.' }, - ], - response: '{\n "success": true,\n "obj": {\n "email": "user1",\n "up": 1048576,\n "down": 2097152,\n "total": 10737418240,\n "expiryTime": 1735689600000\n }\n}', - }, { method: 'POST', path: '/panel/api/inbounds/add', @@ -140,59 +122,6 @@ export const sections = [ ], body: '{\n "enable": false\n}', }, - { - method: 'POST', - path: '/panel/api/inbounds/clientIps/:email', - summary: 'List source IPs that have connected with the given client’s credentials. Returns an array of "ip (timestamp)" strings.', - params: [ - { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, - ], - }, - { - method: 'POST', - path: '/panel/api/inbounds/clearClientIps/:email', - summary: 'Reset the recorded IP list for a client.', - params: [ - { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, - ], - }, - { - method: 'POST', - path: '/panel/api/inbounds/addClient', - summary: 'Add one or more clients to an existing inbound. The settings field is the JSON-encoded settings.clients array of the target inbound.', - body: - '{\n "id": 1,\n "settings": "{\\"clients\\":[{\\"id\\":\\"uuid-here\\",\\"email\\":\\"newuser\\",\\"limitIp\\":0,\\"totalGB\\":0,\\"expiryTime\\":0,\\"enable\\":true,\\"flow\\":\\"\\"}]}"\n}', - }, - { - method: 'POST', - path: '/panel/api/inbounds/:id/copyClients', - summary: 'Copy selected clients from one inbound into another. Useful for duplicating user lists across protocols.', - params: [ - { name: 'id', in: 'path', type: 'number', desc: 'Target inbound ID.' }, - { name: 'sourceInboundId', in: 'body', type: 'number', desc: 'Inbound ID to read clients from.' }, - { name: 'clientEmails', in: 'body', type: 'string[]', desc: 'Emails of clients to copy. Empty means all clients.' }, - { name: 'flow', in: 'body', type: 'string', desc: 'Override the flow field on copied clients (e.g. "xtls-rprx-vision"). Empty to keep source flow.' }, - ], - }, - { - method: 'POST', - path: '/panel/api/inbounds/:id/delClient/:clientId', - summary: 'Delete a client by its UUID/password from a specific inbound.', - params: [ - { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, - { name: 'clientId', in: 'path', type: 'string', desc: 'Client UUID / password.' }, - ], - }, - { - method: 'POST', - path: '/panel/api/inbounds/updateClient/:clientId', - summary: 'Update a single client without rewriting the whole settings JSON. Send the target inbound payload with the new client values.', - params: [ - { name: 'clientId', in: 'path', type: 'string', desc: 'Client UUID / password.' }, - ], - body: - '{\n "id": 1,\n "settings": "{\\"clients\\":[{\\"id\\":\\"uuid-here\\",\\"email\\":\\"user1\\",\\"limitIp\\":2,\\"totalGB\\":10737418240,\\"expiryTime\\":1735689600000,\\"enable\\":true}]}"\n}', - }, { method: 'POST', path: '/panel/api/inbounds/:id/resetTraffic', @@ -201,36 +130,11 @@ export const sections = [ { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, ], }, - { - method: 'POST', - path: '/panel/api/inbounds/:id/resetClientTraffic/:email', - summary: 'Zero out upload + download counters for one client.', - params: [ - { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, - { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, - ], - }, { method: 'POST', path: '/panel/api/inbounds/resetAllTraffics', summary: 'Reset upload + download counters on every inbound. Destructive — accounting history is lost.', }, - { - method: 'POST', - path: '/panel/api/inbounds/resetAllClientTraffics/:id', - summary: 'Reset traffic for every client in one inbound.', - params: [ - { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, - ], - }, - { - method: 'POST', - path: '/panel/api/inbounds/delDepletedClients/:id', - summary: 'Delete clients in this inbound whose traffic cap or expiry has elapsed. Pass id=-1 to sweep every inbound.', - params: [ - { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID, or -1 for all inbounds.' }, - ], - }, { method: 'POST', path: '/panel/api/inbounds/import', @@ -239,59 +143,6 @@ export const sections = [ { name: 'data', in: 'body (form)', type: 'string', desc: 'JSON-encoded inbound payload.' }, ], }, - { - method: 'POST', - path: '/panel/api/inbounds/onlines', - summary: 'List the emails of currently connected clients (last seen within the heartbeat window).', - response: '{\n "success": true,\n "obj": ["user1", "user2"]\n}', - }, - { - method: 'POST', - path: '/panel/api/inbounds/lastOnline', - summary: 'Map of client email → last-seen unix timestamp.', - response: '{\n "success": true,\n "obj": [\n { "email": "user1", "lastOnline": 1700000000 },\n { "email": "user2", "lastOnline": 1699999000 }\n ]\n}', - }, - { - method: 'GET', - path: '/panel/api/inbounds/getSubLinks/:subId', - summary: - 'Return every protocol URL (vless://, vmess://, trojan://, ss://, hysteria://, hy2://) for clients matching the subscription ID. Same result set as /sub/, but as a JSON array — no base64. When an inbound has streamSettings.externalProxy set, one URL is emitted per external proxy. Empty array when the subId has no enabled clients.', - params: [ - { name: 'subId', in: 'path', type: 'string', desc: "Subscription ID, taken from the client's subId field." }, - ], - response: - '{\n "success": true,\n "obj": [\n "vless://uuid@host:443?security=reality&...#user1",\n "vmess://eyJ2IjoyLC..."\n ]\n}', - }, - { - method: 'GET', - path: '/panel/api/inbounds/getClientLinks/:id/:email', - summary: - "Return the URL(s) for one client on one inbound — the same string the Copy URL button copies in the panel UI. Supported protocols: vmess, vless, trojan, shadowsocks, hysteria, hysteria2. If streamSettings.externalProxy is set, returns one URL per external proxy. Protocols without a URL form (socks, http, mixed, wireguard, dokodemo, tunnel) return an empty array.", - params: [ - { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, - { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, - ], - response: - '{\n "success": true,\n "obj": [\n "vless://uuid@host:443?...#user1"\n ]\n}', - }, - { - method: 'POST', - path: '/panel/api/inbounds/updateClientTraffic/:email', - summary: 'Manually adjust a client’s upload + download counters. Useful for migrations from external accounting systems.', - params: [ - { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, - ], - body: '{\n "upload": 1073741824,\n "download": 5368709120\n}', - }, - { - method: 'POST', - path: '/panel/api/inbounds/:id/delClientByEmail/:email', - summary: 'Delete a client identified by email rather than UUID.', - params: [ - { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, - { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, - ], - }, { method: 'GET', path: '/panel/api/inbounds/:id/fallbackChildren', @@ -603,6 +454,92 @@ export const sections = [ summary: 'Delete every client whose traffic quota is exhausted (used >= total, when reset is disabled) or whose expiry has passed. Returns the deleted count and triggers an Xray restart when any client was on a running inbound.', response: '{\n "success": true,\n "obj": {\n "deleted": 0\n }\n}', }, + { + method: 'POST', + path: '/panel/api/clients/resetTraffic/:email', + summary: 'Zero out a single client’s up/down counters. Re-enables the client across every attached inbound and pushes the change to Xray (or the remote node) so depleted users can connect again immediately.', + params: [ + { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/clients/updateTraffic/:email', + summary: 'Manually adjust a client’s upload + download counters. Useful for migrations from external accounting systems.', + params: [ + { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, + ], + body: '{\n "upload": 1073741824,\n "download": 5368709120\n}', + }, + { + method: 'POST', + path: '/panel/api/clients/clientIps/:email', + summary: 'List source IPs that have connected with the given client’s credentials. Returns an array of "ip (timestamp)" strings.', + params: [ + { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/clients/clearClientIps/:email', + summary: 'Reset the recorded IP list for a client.', + params: [ + { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/clients/onlines', + summary: 'List the emails of currently connected clients (last seen within the heartbeat window).', + response: '{\n "success": true,\n "obj": ["user1", "user2"]\n}', + }, + { + method: 'POST', + path: '/panel/api/clients/lastOnline', + summary: 'Map of client email → last-seen unix timestamp.', + response: '{\n "success": true,\n "obj": {\n "user1": 1700000000,\n "user2": 1699999000\n }\n}', + }, + { + method: 'GET', + path: '/panel/api/clients/traffic/:email', + summary: 'Traffic counters for a client identified by email.', + params: [ + { name: 'email', in: 'path', type: 'string', desc: 'Client email (unique across the panel).' }, + ], + response: '{\n "success": true,\n "obj": {\n "email": "user1",\n "up": 1048576,\n "down": 2097152,\n "total": 10737418240,\n "expiryTime": 1735689600000\n }\n}', + }, + { + method: 'GET', + path: '/panel/api/clients/traffic/byId/:id', + summary: 'Traffic counters for a client identified by its UUID/password.', + params: [ + { name: 'id', in: 'path', type: 'string', desc: 'Client UUID / password.' }, + ], + response: '{\n "success": true,\n "obj": [\n {\n "email": "user1",\n "up": 1048576,\n "down": 2097152,\n "total": 10737418240,\n "expiryTime": 1735689600000\n }\n ]\n}', + }, + { + method: 'GET', + path: '/panel/api/clients/subLinks/:subId', + summary: + 'Return every protocol URL (vless://, vmess://, trojan://, ss://, hysteria://, hy2://) for clients matching the subscription ID. Same result set as /sub/, but as a JSON array — no base64. When an inbound has streamSettings.externalProxy set, one URL is emitted per external proxy. Empty array when the subId has no enabled clients.', + params: [ + { name: 'subId', in: 'path', type: 'string', desc: "Subscription ID, taken from the client's subId field." }, + ], + response: + '{\n "success": true,\n "obj": [\n "vless://uuid@host:443?security=reality&...#user1",\n "vmess://eyJ2IjoyLC..."\n ]\n}', + }, + { + method: 'GET', + path: '/panel/api/clients/links/:id/:email', + summary: + "Return the URL(s) for one client on one inbound — the same string the Copy URL button copies in the panel UI. Supported protocols: vmess, vless, trojan, shadowsocks, hysteria, hysteria2. If streamSettings.externalProxy is set, returns one URL per external proxy. Protocols without a URL form (socks, http, mixed, wireguard, dokodemo, tunnel) return an empty array.", + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, + { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, + ], + response: + '{\n "success": true,\n "obj": [\n "vless://uuid@host:443?...#user1"\n ]\n}', + }, ], }, diff --git a/frontend/src/pages/clients/ClientInfoModal.vue b/frontend/src/pages/clients/ClientInfoModal.vue index d1151fff..a7f818d5 100644 --- a/frontend/src/pages/clients/ClientInfoModal.vue +++ b/frontend/src/pages/clients/ClientInfoModal.vue @@ -81,7 +81,7 @@ async function loadLinks() { linksLoading.value = true; try { const msg = await HttpUtil.get( - `/panel/api/inbounds/getSubLinks/${encodeURIComponent(props.client.subId)}`, + `/panel/api/clients/subLinks/${encodeURIComponent(props.client.subId)}`, ); links.value = msg?.success && Array.isArray(msg.obj) ? msg.obj : []; } finally { diff --git a/frontend/src/pages/clients/ClientQrModal.vue b/frontend/src/pages/clients/ClientQrModal.vue index 815dd228..56f1f324 100644 --- a/frontend/src/pages/clients/ClientQrModal.vue +++ b/frontend/src/pages/clients/ClientQrModal.vue @@ -50,7 +50,7 @@ watch(() => props.open, async (next) => { } loading.value = true; try { - const msg = await HttpUtil.get(`/panel/api/inbounds/getSubLinks/${encodeURIComponent(props.client.subId)}`); + const msg = await HttpUtil.get(`/panel/api/clients/subLinks/${encodeURIComponent(props.client.subId)}`); links.value = msg?.success && Array.isArray(msg.obj) ? msg.obj : []; } finally { loading.value = false; diff --git a/frontend/src/pages/clients/useClients.js b/frontend/src/pages/clients/useClients.js index 73c4cc6c..a46df741 100644 --- a/frontend/src/pages/clients/useClients.js +++ b/frontend/src/pages/clients/useClients.js @@ -45,7 +45,7 @@ export function useClients() { } async function refreshOnlines() { - const msg = await HttpUtil.post('/panel/api/inbounds/onlines'); + const msg = await HttpUtil.post('/panel/api/clients/onlines'); if (msg?.success) { onlines.value = Array.isArray(msg.obj) ? msg.obj : []; } @@ -85,9 +85,8 @@ export function useClients() { } async function resetTraffic(client) { - const ibIds = Array.isArray(client?.inboundIds) ? client.inboundIds : []; - if (!client?.email || ibIds.length === 0) return null; - const url = `/panel/api/inbounds/${ibIds[0]}/resetClientTraffic/${encodeURIComponent(client.email)}`; + if (!client?.email) return null; + const url = `/panel/api/clients/resetTraffic/${encodeURIComponent(client.email)}`; const msg = await HttpUtil.post(url); if (msg?.success) await refresh(); return msg; diff --git a/frontend/src/pages/inbounds/InboundInfoModal.vue b/frontend/src/pages/inbounds/InboundInfoModal.vue index 61ce2fcf..b557c574 100644 --- a/frontend/src/pages/inbounds/InboundInfoModal.vue +++ b/frontend/src/pages/inbounds/InboundInfoModal.vue @@ -137,7 +137,7 @@ async function loadClientIps() { if (!clientStats.value?.email) return; refreshing.value = true; try { - const msg = await HttpUtil.post(`/panel/api/inbounds/clientIps/${clientStats.value.email}`); + const msg = await HttpUtil.post(`/panel/api/clients/clientIps/${clientStats.value.email}`); if (!msg?.success) { clientIpsText.value = msg?.obj || 'No IP record'; clientIpsArray.value = []; @@ -164,7 +164,7 @@ async function loadClientIps() { async function clearClientIps() { if (!clientStats.value?.email) return; - const msg = await HttpUtil.post(`/panel/api/inbounds/clearClientIps/${clientStats.value.email}`); + const msg = await HttpUtil.post(`/panel/api/clients/clearClientIps/${clientStats.value.email}`); if (msg?.success) { clientIpsArray.value = []; clientIpsText.value = t('tgbot.noIpRecord'); diff --git a/frontend/src/pages/inbounds/InboundList.vue b/frontend/src/pages/inbounds/InboundList.vue index 00352c26..0ddd0ce1 100644 --- a/frontend/src/pages/inbounds/InboundList.vue +++ b/frontend/src/pages/inbounds/InboundList.vue @@ -10,11 +10,9 @@ import { EditOutlined, QrcodeOutlined, CopyOutlined, - FileDoneOutlined, ExportOutlined, ImportOutlined, ReloadOutlined, - RestOutlined, RetweetOutlined, BlockOutlined, DeleteOutlined, @@ -317,12 +315,6 @@ function showQrCodeMenu(dbInbound) { {{ t('pages.inbounds.resetAllTraffic') }} - - {{ t('pages.inbounds.resetAllClientTraffics') }} - - - {{ t('pages.inbounds.delDepletedClients') }} - @@ -390,18 +382,12 @@ function showQrCodeMenu(dbInbound) { {{ t('qrCode') }}