2026-05-17 05:28:55 +00:00
|
|
|
package controller
|
|
|
|
|
|
|
|
|
|
import (
|
refactor(api): move every client-shaped endpoint off /inbounds onto /clients
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 <noreply@anthropic.com>
2026-05-17 08:15:01 +00:00
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"time"
|
2026-05-17 05:28:55 +00:00
|
|
|
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/database/model"
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/web/service"
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type ClientController struct {
|
|
|
|
|
clientService service.ClientService
|
|
|
|
|
inboundService service.InboundService
|
|
|
|
|
xrayService service.XrayService
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewClientController(g *gin.RouterGroup) *ClientController {
|
|
|
|
|
a := &ClientController{}
|
|
|
|
|
a.initRouter(g)
|
|
|
|
|
return a
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *ClientController) initRouter(g *gin.RouterGroup) {
|
|
|
|
|
g.GET("/list", a.list)
|
refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:31:38 +00:00
|
|
|
g.GET("/get/:email", a.get)
|
2026-05-17 17:04:43 +00:00
|
|
|
g.GET("/traffic/:email", a.getTrafficByEmail)
|
|
|
|
|
g.GET("/subLinks/:subId", a.getSubLinks)
|
|
|
|
|
g.GET("/links/:email", a.getClientLinks)
|
|
|
|
|
|
2026-05-17 05:28:55 +00:00
|
|
|
g.POST("/add", a.create)
|
refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:31:38 +00:00
|
|
|
g.POST("/update/:email", a.update)
|
|
|
|
|
g.POST("/del/:email", a.delete)
|
|
|
|
|
g.POST("/:email/attach", a.attach)
|
|
|
|
|
g.POST("/:email/detach", a.detach)
|
2026-05-17 06:25:38 +00:00
|
|
|
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
2026-05-17 07:45:38 +00:00
|
|
|
g.POST("/delDepleted", a.delDepleted)
|
refactor(api): move every client-shaped endpoint off /inbounds onto /clients
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 <noreply@anthropic.com>
2026-05-17 08:15:01 +00:00
|
|
|
g.POST("/resetTraffic/:email", a.resetTrafficByEmail)
|
|
|
|
|
g.POST("/updateTraffic/:email", a.updateTrafficByEmail)
|
2026-05-17 09:25:24 +00:00
|
|
|
g.POST("/ips/:email", a.getIps)
|
|
|
|
|
g.POST("/clearIps/:email", a.clearIps)
|
refactor(api): move every client-shaped endpoint off /inbounds onto /clients
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 <noreply@anthropic.com>
2026-05-17 08:15:01 +00:00
|
|
|
g.POST("/onlines", a.onlines)
|
|
|
|
|
g.POST("/lastOnline", a.lastOnline)
|
2026-05-17 05:28:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *ClientController) list(c *gin.Context) {
|
|
|
|
|
rows, err := a.clientService.List()
|
|
|
|
|
if err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonObj(c, rows, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *ClientController) get(c *gin.Context) {
|
refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:31:38 +00:00
|
|
|
email := c.Param("email")
|
|
|
|
|
rec, err := a.clientService.GetRecordByEmail(nil, email)
|
2026-05-17 05:28:55 +00:00
|
|
|
if err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "get"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:31:38 +00:00
|
|
|
inboundIds, err := a.clientService.GetInboundIdsForRecord(rec.Id)
|
2026-05-17 05:28:55 +00:00
|
|
|
if err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "get"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonObj(c, gin.H{"client": rec, "inboundIds": inboundIds}, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *ClientController) create(c *gin.Context) {
|
|
|
|
|
var payload service.ClientCreatePayload
|
|
|
|
|
if err := c.ShouldBindJSON(&payload); err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
needRestart, err := a.clientService.Create(&a.inboundService, &payload)
|
|
|
|
|
if err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientAddSuccess"), nil)
|
|
|
|
|
if needRestart {
|
|
|
|
|
a.xrayService.SetToNeedRestart()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *ClientController) update(c *gin.Context) {
|
refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:31:38 +00:00
|
|
|
email := c.Param("email")
|
2026-05-17 05:28:55 +00:00
|
|
|
var updated model.Client
|
|
|
|
|
if err := c.ShouldBindJSON(&updated); err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:31:38 +00:00
|
|
|
needRestart, err := a.clientService.UpdateByEmail(&a.inboundService, email, updated)
|
2026-05-17 05:28:55 +00:00
|
|
|
if err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
|
|
|
|
|
if needRestart {
|
|
|
|
|
a.xrayService.SetToNeedRestart()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *ClientController) delete(c *gin.Context) {
|
refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:31:38 +00:00
|
|
|
email := c.Param("email")
|
2026-05-17 05:28:55 +00:00
|
|
|
keepTraffic := c.Query("keepTraffic") == "1"
|
refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:31:38 +00:00
|
|
|
needRestart, err := a.clientService.DeleteByEmail(&a.inboundService, email, keepTraffic)
|
2026-05-17 05:28:55 +00:00
|
|
|
if err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientDeleteSuccess"), nil)
|
|
|
|
|
if needRestart {
|
|
|
|
|
a.xrayService.SetToNeedRestart()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type attachDetachBody struct {
|
|
|
|
|
InboundIds []int `json:"inboundIds"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *ClientController) attach(c *gin.Context) {
|
refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:31:38 +00:00
|
|
|
email := c.Param("email")
|
2026-05-17 05:28:55 +00:00
|
|
|
var body attachDetachBody
|
|
|
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:31:38 +00:00
|
|
|
needRestart, err := a.clientService.AttachByEmail(&a.inboundService, email, body.InboundIds)
|
2026-05-17 05:28:55 +00:00
|
|
|
if err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientAddSuccess"), nil)
|
|
|
|
|
if needRestart {
|
|
|
|
|
a.xrayService.SetToNeedRestart()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 06:25:38 +00:00
|
|
|
func (a *ClientController) resetAllTraffics(c *gin.Context) {
|
|
|
|
|
needRestart, err := a.clientService.ResetAllTraffics()
|
|
|
|
|
if err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllClientTrafficSuccess"), nil)
|
|
|
|
|
if needRestart {
|
|
|
|
|
a.xrayService.SetToNeedRestart()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 07:45:38 +00:00
|
|
|
func (a *ClientController) delDepleted(c *gin.Context) {
|
|
|
|
|
deleted, needRestart, err := a.clientService.DelDepleted(&a.inboundService)
|
|
|
|
|
if err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonObj(c, gin.H{"deleted": deleted}, nil)
|
|
|
|
|
if needRestart {
|
|
|
|
|
a.xrayService.SetToNeedRestart()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(api): move every client-shaped endpoint off /inbounds onto /clients
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 <noreply@anthropic.com>
2026-05-17 08:15:01 +00:00
|
|
|
func (a *ClientController) resetTrafficByEmail(c *gin.Context) {
|
|
|
|
|
email := c.Param("email")
|
|
|
|
|
needRestart, err := a.clientService.ResetTrafficByEmail(&a.inboundService, email)
|
|
|
|
|
if err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetInboundClientTrafficSuccess"), nil)
|
|
|
|
|
if needRestart {
|
|
|
|
|
a.xrayService.SetToNeedRestart()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type trafficUpdateRequest struct {
|
|
|
|
|
Upload int64 `json:"upload"`
|
|
|
|
|
Download int64 `json:"download"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *ClientController) updateTrafficByEmail(c *gin.Context) {
|
|
|
|
|
email := c.Param("email")
|
|
|
|
|
var req trafficUpdateRequest
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := a.inboundService.UpdateClientTrafficByEmail(email, req.Upload, req.Download); err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 09:25:24 +00:00
|
|
|
func (a *ClientController) getIps(c *gin.Context) {
|
refactor(api): move every client-shaped endpoint off /inbounds onto /clients
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 <noreply@anthropic.com>
2026-05-17 08:15:01 +00:00
|
|
|
email := c.Param("email")
|
|
|
|
|
ips, err := a.inboundService.GetInboundClientIps(email)
|
|
|
|
|
if err != nil || ips == "" {
|
|
|
|
|
jsonObj(c, "No IP Record", nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
type ipWithTimestamp struct {
|
|
|
|
|
IP string `json:"ip"`
|
|
|
|
|
Timestamp int64 `json:"timestamp"`
|
|
|
|
|
}
|
|
|
|
|
var ipsWithTime []ipWithTimestamp
|
|
|
|
|
if err := json.Unmarshal([]byte(ips), &ipsWithTime); err == nil && len(ipsWithTime) > 0 {
|
|
|
|
|
formatted := make([]string, 0, len(ipsWithTime))
|
|
|
|
|
for _, item := range ipsWithTime {
|
|
|
|
|
if item.IP == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if item.Timestamp > 0 {
|
|
|
|
|
ts := time.Unix(item.Timestamp, 0).Local().Format("2006-01-02 15:04:05")
|
|
|
|
|
formatted = append(formatted, fmt.Sprintf("%s (%s)", item.IP, ts))
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
formatted = append(formatted, item.IP)
|
|
|
|
|
}
|
|
|
|
|
jsonObj(c, formatted, nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var oldIps []string
|
|
|
|
|
if err := json.Unmarshal([]byte(ips), &oldIps); err == nil && len(oldIps) > 0 {
|
|
|
|
|
jsonObj(c, oldIps, nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonObj(c, ips, nil)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 09:25:24 +00:00
|
|
|
func (a *ClientController) clearIps(c *gin.Context) {
|
refactor(api): move every client-shaped endpoint off /inbounds onto /clients
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 <noreply@anthropic.com>
2026-05-17 08:15:01 +00:00
|
|
|
email := c.Param("email")
|
|
|
|
|
if err := a.inboundService.ClearClientIps(email); err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.updateSuccess"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.logCleanSuccess"), nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *ClientController) onlines(c *gin.Context) {
|
|
|
|
|
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *ClientController) lastOnline(c *gin.Context) {
|
|
|
|
|
data, err := a.inboundService.GetClientsLastOnline()
|
|
|
|
|
jsonObj(c, data, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *ClientController) getTrafficByEmail(c *gin.Context) {
|
|
|
|
|
email := c.Param("email")
|
|
|
|
|
traffic, err := a.inboundService.GetClientTrafficByEmail(email)
|
|
|
|
|
if err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.trafficGetError"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonObj(c, traffic, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *ClientController) getSubLinks(c *gin.Context) {
|
|
|
|
|
links, err := a.inboundService.GetSubLinks(resolveHost(c), c.Param("subId"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonObj(c, links, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *ClientController) getClientLinks(c *gin.Context) {
|
refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:31:38 +00:00
|
|
|
links, err := a.inboundService.GetAllClientLinks(resolveHost(c), c.Param("email"))
|
refactor(api): move every client-shaped endpoint off /inbounds onto /clients
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 <noreply@anthropic.com>
2026-05-17 08:15:01 +00:00
|
|
|
if err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonObj(c, links, nil)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 05:28:55 +00:00
|
|
|
func (a *ClientController) detach(c *gin.Context) {
|
refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:31:38 +00:00
|
|
|
email := c.Param("email")
|
2026-05-17 05:28:55 +00:00
|
|
|
var body attachDetachBody
|
|
|
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:31:38 +00:00
|
|
|
needRestart, err := a.clientService.DetachByEmailMany(&a.inboundService, email, body.InboundIds)
|
2026-05-17 05:28:55 +00:00
|
|
|
if err != nil {
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientDeleteSuccess"), nil)
|
|
|
|
|
if needRestart {
|
|
|
|
|
a.xrayService.SetToNeedRestart()
|
|
|
|
|
}
|
|
|
|
|
}
|