3x-ui/web/controller/client.go

297 lines
8.2 KiB
Go
Raw Normal View History

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"
"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)
g.GET("/get/:email", a.get)
g.POST("/add", a.create)
g.POST("/update/:email", a.update)
g.POST("/del/:email", a.delete)
g.POST("/:email/attach", a.attach)
g.POST("/:email/detach", a.detach)
g.POST("/resetAllTraffics", a.resetAllTraffics)
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)
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)
g.GET("/traffic/:email", a.getTrafficByEmail)
g.GET("/subLinks/:subId", a.getSubLinks)
g.GET("/links/:email", a.getClientLinks)
}
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) {
email := c.Param("email")
rec, err := a.clientService.GetRecordByEmail(nil, email)
if err != nil {
jsonMsg(c, I18nWeb(c, "get"), err)
return
}
inboundIds, err := a.clientService.GetInboundIdsForRecord(rec.Id)
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) {
email := c.Param("email")
var updated model.Client
if err := c.ShouldBindJSON(&updated); err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
needRestart, err := a.clientService.UpdateByEmail(&a.inboundService, email, updated)
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) {
email := c.Param("email")
keepTraffic := c.Query("keepTraffic") == "1"
needRestart, err := a.clientService.DeleteByEmail(&a.inboundService, email, keepTraffic)
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) {
email := c.Param("email")
var body attachDetachBody
if err := c.ShouldBindJSON(&body); err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
needRestart, err := a.clientService.AttachByEmail(&a.inboundService, email, body.InboundIds)
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) 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()
}
}
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)
}
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)
}
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) {
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)
}
func (a *ClientController) detach(c *gin.Context) {
email := c.Param("email")
var body attachDetachBody
if err := c.ShouldBindJSON(&body); err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
needRestart, err := a.clientService.DetachByEmailMany(&a.inboundService, email, body.InboundIds)
if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientDeleteSuccess"), nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
}