mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-24 06:45:01 +03:00
feat(api): add client lookup by subscription ID (#6908)
Integrations could only find a client by subscription ID through a substring search over the paged client list followed by a second lookup by email. GET /panel/api/clients/get/subId/:subId now returns the full client records (settings, attached inbounds, external links, traffic, tunnel addresses) for an exact subId match, as a list because older rows may share an ID. It sits behind the same admin login or admin-token check as the email lookup.
This commit is contained in:
@@ -28,6 +28,9 @@ _openapi:
|
||||
title: Fetch one client by email, including the inbound IDs and external config
|
||||
IDs it is attached to.
|
||||
url: '#fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to'
|
||||
- depth: 2
|
||||
title: Fetch clients by subscription ID.
|
||||
url: '#fetch-clients-by-subscription-id'
|
||||
- depth: 2
|
||||
title: Fetch clients by Telegram user ID. Returns an array since multiple
|
||||
clients can share the same Telegram ID.
|
||||
@@ -319,6 +322,8 @@ _openapi:
|
||||
- content: Fetch one client by email, including the inbound IDs and external
|
||||
config IDs it is attached to.
|
||||
id: fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
|
||||
- content: Fetch clients by subscription ID.
|
||||
id: fetch-clients-by-subscription-id
|
||||
- content: Fetch clients by Telegram user ID. Returns an array since multiple
|
||||
clients can share the same Telegram ID.
|
||||
id: fetch-clients-by-telegram-user-id-returns-an-array-since-multiple-clients-can-share-the-same-telegram-id
|
||||
@@ -555,6 +560,13 @@ _openapi:
|
||||
tunnel) contribute nothing.'
|
||||
id: return-every-url-for-one-client-across-all-attached-inbounds-one-per-advertised-endpoint-the-managed-hosts-of-the-inbound-else-its-streamsettingsexternalproxy-entries-else-its-own-address-supported-protocols-vmess-vless-trojan-shadowsocks-hysteria-mtproto-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing
|
||||
contents:
|
||||
- content: Matches the exact subscription ID and orders results by client ID.
|
||||
Returns an array of the same payloads as GET /clients/get/:email,
|
||||
since multiple clients can share a subscription ID. Includes disabled,
|
||||
expired and unattached clients. An unknown ID returns HTTP 200 with
|
||||
success:false, obj:null and a record-not-found message, matching the
|
||||
email lookup. Requires an admin token or a panel login session.
|
||||
heading: fetch-clients-by-subscription-id
|
||||
- content: >-
|
||||
Fields the server fills in when they are omitted — a valid value sent
|
||||
by the caller is never overwritten. Re-adding an email that already
|
||||
@@ -667,7 +679,7 @@ export default function Layout(props) {
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/clients/list","method":"get"},{"path":"/panel/api/clients/list/paged","method":"get"},{"path":"/panel/api/clients/get/{email}","method":"get"},{"path":"/panel/api/clients/get/tgId/{tgId}","method":"get"},{"path":"/panel/api/clients/add","method":"post"},{"path":"/panel/api/clients/renewalPreview","method":"post"},{"path":"/panel/api/clients/update/{email}","method":"post"},{"path":"/panel/api/clients/del/{email}","method":"post"},{"path":"/panel/api/clients/{email}/attach","method":"post"},{"path":"/panel/api/clients/{email}/detach","method":"post"},{"path":"/panel/api/clients/{email}/externalLinks","method":"post"},{"path":"/panel/api/clients/resetAllTraffics","method":"post"},{"path":"/panel/api/clients/delDepleted","method":"post"},{"path":"/panel/api/clients/delOrphans","method":"post"},{"path":"/panel/api/clients/export","method":"get"},{"path":"/panel/api/clients/import","method":"post"},{"path":"/panel/api/clients/bulkAdjust","method":"post"},{"path":"/panel/api/clients/bulkEnable","method":"post"},{"path":"/panel/api/clients/bulkDisable","method":"post"},{"path":"/panel/api/clients/bulkDel","method":"post"},{"path":"/panel/api/clients/bulkCreate","method":"post"},{"path":"/panel/api/clients/groups/bulkAdd","method":"post"},{"path":"/panel/api/clients/groups/bulkRemove","method":"post"},{"path":"/panel/api/clients/bulkAttach","method":"post"},{"path":"/panel/api/clients/bulkDetach","method":"post"},{"path":"/panel/api/clients/bulkResetTraffic","method":"post"},{"path":"/panel/api/clients/groups","method":"get"},{"path":"/panel/api/clients/groups/{name}/emails","method":"get"},{"path":"/panel/api/clients/groups/create","method":"post"},{"path":"/panel/api/clients/groups/rename","method":"post"},{"path":"/panel/api/clients/groups/delete","method":"post"},{"path":"/panel/api/clients/groups/resetTraffic","method":"post"},{"path":"/panel/api/clients/resetTraffic/{email}","method":"post"},{"path":"/panel/api/clients/updateTraffic/{email}","method":"post"},{"path":"/panel/api/clients/ips/{email}","method":"post"},{"path":"/panel/api/clients/clearIps/{email}","method":"post"},{"path":"/panel/api/clients/hwids/{email}","method":"post"},{"path":"/panel/api/clients/hwids/{email}","method":"delete"},{"path":"/panel/api/clients/hwids/{email}/{id}","method":"delete"},{"path":"/panel/api/clients/onlines","method":"post"},{"path":"/panel/api/clients/onlinesByGuid","method":"post"},{"path":"/panel/api/clients/clientIpsByGuid","method":"post"},{"path":"/panel/api/clients/activeInbounds","method":"post"},{"path":"/panel/api/clients/lastOnline","method":"post"},{"path":"/panel/api/clients/traffic/{email}","method":"get"},{"path":"/panel/api/clients/subLinks/{subId}","method":"get"},{"path":"/panel/api/clients/happLink/{id}","method":"post"},{"path":"/panel/api/clients/links/{email}","method":"get"}]} showTitle />
|
||||
<OpenAPIPage document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/clients/list","method":"get"},{"path":"/panel/api/clients/list/paged","method":"get"},{"path":"/panel/api/clients/get/{email}","method":"get"},{"path":"/panel/api/clients/get/subId/{subId}","method":"get"},{"path":"/panel/api/clients/get/tgId/{tgId}","method":"get"},{"path":"/panel/api/clients/add","method":"post"},{"path":"/panel/api/clients/renewalPreview","method":"post"},{"path":"/panel/api/clients/update/{email}","method":"post"},{"path":"/panel/api/clients/del/{email}","method":"post"},{"path":"/panel/api/clients/{email}/attach","method":"post"},{"path":"/panel/api/clients/{email}/detach","method":"post"},{"path":"/panel/api/clients/{email}/externalLinks","method":"post"},{"path":"/panel/api/clients/resetAllTraffics","method":"post"},{"path":"/panel/api/clients/delDepleted","method":"post"},{"path":"/panel/api/clients/delOrphans","method":"post"},{"path":"/panel/api/clients/export","method":"get"},{"path":"/panel/api/clients/import","method":"post"},{"path":"/panel/api/clients/bulkAdjust","method":"post"},{"path":"/panel/api/clients/bulkEnable","method":"post"},{"path":"/panel/api/clients/bulkDisable","method":"post"},{"path":"/panel/api/clients/bulkDel","method":"post"},{"path":"/panel/api/clients/bulkCreate","method":"post"},{"path":"/panel/api/clients/groups/bulkAdd","method":"post"},{"path":"/panel/api/clients/groups/bulkRemove","method":"post"},{"path":"/panel/api/clients/bulkAttach","method":"post"},{"path":"/panel/api/clients/bulkDetach","method":"post"},{"path":"/panel/api/clients/bulkResetTraffic","method":"post"},{"path":"/panel/api/clients/groups","method":"get"},{"path":"/panel/api/clients/groups/{name}/emails","method":"get"},{"path":"/panel/api/clients/groups/create","method":"post"},{"path":"/panel/api/clients/groups/rename","method":"post"},{"path":"/panel/api/clients/groups/delete","method":"post"},{"path":"/panel/api/clients/groups/resetTraffic","method":"post"},{"path":"/panel/api/clients/resetTraffic/{email}","method":"post"},{"path":"/panel/api/clients/updateTraffic/{email}","method":"post"},{"path":"/panel/api/clients/ips/{email}","method":"post"},{"path":"/panel/api/clients/clearIps/{email}","method":"post"},{"path":"/panel/api/clients/hwids/{email}","method":"post"},{"path":"/panel/api/clients/hwids/{email}","method":"delete"},{"path":"/panel/api/clients/hwids/{email}/{id}","method":"delete"},{"path":"/panel/api/clients/onlines","method":"post"},{"path":"/panel/api/clients/onlinesByGuid","method":"post"},{"path":"/panel/api/clients/clientIpsByGuid","method":"post"},{"path":"/panel/api/clients/activeInbounds","method":"post"},{"path":"/panel/api/clients/lastOnline","method":"post"},{"path":"/panel/api/clients/traffic/{email}","method":"get"},{"path":"/panel/api/clients/subLinks/{subId}","method":"get"},{"path":"/panel/api/clients/happLink/{id}","method":"post"},{"path":"/panel/api/clients/links/{email}","method":"get"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -8501,6 +8501,81 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/clients/get/subId/{subId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Clients"
|
||||
],
|
||||
"summary": "Fetch clients by subscription ID.",
|
||||
"operationId": "get_panel_api_clients_get_subId_subId",
|
||||
"description": "Matches the exact subscription ID and orders results by client ID. Returns an array of the same payloads as GET /clients/get/:email, since multiple clients can share a subscription ID. Includes disabled, expired and unattached clients. An unknown ID returns HTTP 200 with success:false, obj:null and a record-not-found message, matching the email lookup. Requires an admin token or a panel login session.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "subId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Exact subscription ID (URL-encoded).",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {
|
||||
"type": "array",
|
||||
"nullable": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client": {
|
||||
"$ref": "#/components/schemas/ClientRecord"
|
||||
},
|
||||
"inboundIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"externalLinks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"usedTraffic": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"tunnelAllowedIPs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/clients/get/tgId/{tgId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
||||
@@ -8501,6 +8501,81 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/clients/get/subId/{subId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Clients"
|
||||
],
|
||||
"summary": "Fetch clients by subscription ID.",
|
||||
"operationId": "get_panel_api_clients_get_subId_subId",
|
||||
"description": "Matches the exact subscription ID and orders results by client ID. Returns an array of the same payloads as GET /clients/get/:email, since multiple clients can share a subscription ID. Includes disabled, expired and unattached clients. An unknown ID returns HTTP 200 with success:false, obj:null and a record-not-found message, matching the email lookup. Requires an admin token or a panel login session.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "subId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Exact subscription ID (URL-encoded).",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {
|
||||
"type": "array",
|
||||
"nullable": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client": {
|
||||
"$ref": "#/components/schemas/ClientRecord"
|
||||
},
|
||||
"inboundIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"externalLinks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"usedTraffic": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"tunnelAllowedIPs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/clients/get/tgId/{tgId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
||||
@@ -1142,6 +1142,35 @@ export const sections: readonly Section[] = [
|
||||
response:
|
||||
'{\n "success": true,\n "obj": {\n "client": { "id": 1, "email": "alice@example.com", ... },\n "inboundIds": [3, 5],\n "externalLinks": [\n { "id": 11, "kind": "link", "value": "vless://...", "remark": "DE", "enable": true, "expiryTime": 0 },\n { "id": 12, "kind": "subscription", "value": "https://provider.example/sub/abc", "remark": "Provider", "enable": false, "expiryTime": 1767225600000, "namePrefix": "[zjh] ", "lastFetchAt": 1767220000000, "lastFetchError": "" }\n ]\n }\n}',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/api/clients/get/subId/:subId',
|
||||
summary: 'Fetch clients by subscription ID.',
|
||||
description:
|
||||
'Matches the exact subscription ID and orders results by client ID. Returns an array of the same payloads as GET /clients/get/:email, since multiple clients can share a subscription ID. Includes disabled, expired and unattached clients. An unknown ID returns HTTP 200 with success:false, obj:null and a record-not-found message, matching the email lookup. Requires an admin token or a panel login session.',
|
||||
params: [
|
||||
{
|
||||
name: 'subId',
|
||||
in: 'path',
|
||||
type: 'string',
|
||||
desc: 'Exact subscription ID (URL-encoded).',
|
||||
},
|
||||
],
|
||||
responseObjectSchema: {
|
||||
type: 'array',
|
||||
nullable: true,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
client: { $ref: '#/components/schemas/ClientRecord' },
|
||||
inboundIds: { type: 'array', items: { type: 'integer' } },
|
||||
externalLinks: { type: 'array', items: { type: 'object' } },
|
||||
usedTraffic: { type: 'integer', format: 'int64' },
|
||||
tunnelAllowedIPs: { type: 'object', additionalProperties: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/api/clients/get/tgId/:tgId',
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/websocket"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func notifyClientsChanged() {
|
||||
@@ -53,6 +54,7 @@ func (a *ClientController) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/list", a.list)
|
||||
g.GET("/list/paged", a.listPaged)
|
||||
g.GET("/get/:email", a.get)
|
||||
g.GET("/get/subId/:subId", a.getBySubId)
|
||||
g.GET("/get/tgId/:tgId", a.getByTgId)
|
||||
g.GET("/traffic/:email", a.getTrafficByEmail)
|
||||
g.GET("/subLinks/:subId", a.getSubLinks)
|
||||
@@ -172,6 +174,27 @@ func (a *ClientController) get(c *gin.Context) {
|
||||
jsonObj(c, payload, nil)
|
||||
}
|
||||
|
||||
func (a *ClientController) getBySubId(c *gin.Context) {
|
||||
records, err := a.clientService.GetRecordsBySubID(c.Param("subId"))
|
||||
if err == nil && len(records) == 0 {
|
||||
err = gorm.ErrRecordNotFound
|
||||
}
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
||||
return
|
||||
}
|
||||
results := make([]gin.H, 0, len(records))
|
||||
for _, rec := range records {
|
||||
payload, err := a.buildClientPayload(rec)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
||||
return
|
||||
}
|
||||
results = append(results, payload)
|
||||
}
|
||||
jsonObj(c, results, nil)
|
||||
}
|
||||
|
||||
func (a *ClientController) getByTgId(c *gin.Context) {
|
||||
tgIdStr := c.Param("tgId")
|
||||
tgId, err := strconv.ParseInt(tgIdStr, 10, 64)
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/locale"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
)
|
||||
|
||||
func TestClientGetBySubIDHTTP(t *testing.T) {
|
||||
dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
|
||||
db := database.GetDB()
|
||||
clients := []model.ClientRecord{
|
||||
{Email: "alice@example.com", SubID: "shared_%", UUID: "alice-uuid", Enable: true},
|
||||
{Email: "bob@example.com", SubID: "shared_%", UUID: "bob-uuid", ExpiryTime: 1},
|
||||
{Email: "carol@example.com", SubID: "solo", Enable: true},
|
||||
{Email: "shared_%", SubID: "shared_extra", Enable: true},
|
||||
}
|
||||
for i := range clients {
|
||||
if err := db.Create(&clients[i]).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := db.Model(&clients[1]).Update("enable", false).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inbound := model.Inbound{Tag: "lookup", Port: 443, Protocol: model.VLESS, Settings: `{"clients":[]}`}
|
||||
if err := db.Create(&inbound).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&model.ClientInbound{ClientId: clients[0].Id, InboundId: inbound.Id, FlowOverride: "xtls-rprx-vision"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&xray.ClientTraffic{Email: clients[0].Email, Up: 111, Down: 222}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.Use(func(c *gin.Context) {
|
||||
c.Set("I18n", func(_ locale.I18nType, key string, _ ...string) string { return key })
|
||||
c.Next()
|
||||
})
|
||||
NewClientController(router.Group("/panel/api/clients"))
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
subID string
|
||||
emails []string
|
||||
}{
|
||||
{"shared subscription includes disabled orphan", "shared_%", []string{"alice@example.com", "bob@example.com"}},
|
||||
{"single match", "solo", []string{"carol@example.com"}},
|
||||
{"missing subscription", "missing", nil},
|
||||
{"partial subscription is not a match", "shared", nil},
|
||||
{"wildcard is not a match", "%", nil},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/panel/api/clients/get/subId/"+url.PathEscape(tt.subID), nil))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
|
||||
}
|
||||
var response struct {
|
||||
Success bool `json:"success"`
|
||||
Msg string `json:"msg"`
|
||||
Obj []struct {
|
||||
Client model.ClientRecord `json:"client"`
|
||||
InboundIDs []int `json:"inboundIds"`
|
||||
UsedTraffic int64 `json:"usedTraffic"`
|
||||
} `json:"obj"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tt.emails == nil {
|
||||
if response.Success || !strings.Contains(response.Msg, gorm.ErrRecordNotFound.Error()) || response.Obj != nil {
|
||||
t.Fatalf("want record-not-found envelope, got %s", w.Body.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
if !response.Success || len(response.Obj) != len(tt.emails) {
|
||||
t.Fatalf("want %d client payloads, got %s", len(tt.emails), w.Body.String())
|
||||
}
|
||||
for i, payload := range response.Obj {
|
||||
if payload.Client.Email != tt.emails[i] || payload.Client.SubID != tt.subID {
|
||||
t.Fatalf("client %d = %+v, want email %q and subId %q", i, payload.Client, tt.emails[i], tt.subID)
|
||||
}
|
||||
switch payload.Client.Email {
|
||||
case "alice@example.com":
|
||||
if payload.Client.UUID != "alice-uuid" || payload.Client.Flow != "xtls-rprx-vision" || payload.UsedTraffic != 333 || !reflect.DeepEqual(payload.InboundIDs, []int{inbound.Id}) {
|
||||
t.Fatalf("attached client payload = %+v", payload)
|
||||
}
|
||||
case "bob@example.com":
|
||||
if payload.Client.Enable || payload.Client.ExpiryTime != 1 || payload.Client.UUID != "bob-uuid" || len(payload.InboundIDs) != 0 {
|
||||
t.Fatalf("disabled orphan payload = %+v", payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientGetBySubIDRequiresAdmin(t *testing.T) {
|
||||
router, api := newAPIAuthTestEngine(t)
|
||||
NewClientController(router.Group("/base/panel/api/clients", api.checkAPIAuth, api.enforceTokenScope))
|
||||
if err := database.GetDB().Create(&model.ClientRecord{Email: "private@example.com", SubID: "private-sub", UUID: "private-uuid"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, scope := range []string{model.ApiScopeAdmin, model.ApiScopeMonitor, model.ApiScopeNodeSync, model.ApiScopeNodeAdmin} {
|
||||
if err := database.GetDB().Create(&model.ApiToken{Name: scope, Token: crypto.HashTokenSHA256(scope), Enabled: true, Scope: scope}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, tt := range []struct {
|
||||
scope string
|
||||
code int
|
||||
}{
|
||||
{"", http.StatusUnauthorized},
|
||||
{model.ApiScopeMonitor, http.StatusForbidden},
|
||||
{model.ApiScopeNodeSync, http.StatusForbidden},
|
||||
{model.ApiScopeNodeAdmin, http.StatusForbidden},
|
||||
{model.ApiScopeAdmin, http.StatusOK},
|
||||
} {
|
||||
t.Run("scope="+tt.scope, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/base/panel/api/clients/get/subId/private-sub", nil)
|
||||
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
||||
if tt.scope != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+tt.scope)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
if w.Code != tt.code {
|
||||
t.Fatalf("status = %d, want %d, body = %s", w.Code, tt.code, w.Body.String())
|
||||
}
|
||||
hasCredentials := strings.Contains(w.Body.String(), "private-uuid")
|
||||
if hasCredentials != (tt.code == http.StatusOK) {
|
||||
t.Fatalf("unexpected credential access: %s", w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user