package controller import ( "encoding/json" "errors" "fmt" "strconv" "time" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/web/session" "github.com/mhsanaei/3x-ui/v2/web/websocket" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // InboundController handles HTTP requests related to Xray inbounds management. type InboundController struct { inboundService service.InboundService xrayService service.XrayService settingService service.SettingService } // NewInboundController creates a new InboundController and sets up its routes. func NewInboundController(g *gin.RouterGroup) *InboundController { a := &InboundController{} a.initRouter(g) return a } // initRouter initializes the routes for inbound-related operations. func (a *InboundController) initRouter(g *gin.RouterGroup) { g.GET("/list", a.getInbounds) g.GET("/get/:id", a.getInbound) g.GET("/getClientTraffics/:email", a.getClientTraffics) g.GET("/getClientTrafficsById/:id", a.getClientTrafficsById) g.POST("/add", a.addInbound) g.POST("/del/:id", a.delInbound) g.POST("/update/:id", a.updateInbound) g.POST("/clientIps/:email", a.getClientIps) g.POST("/clearClientIps/:email", a.clearClientIps) g.POST("/addClient", a.addInboundClient) g.POST("/:id/delClient/:clientId", a.delInboundClient) g.POST("/updateClient/:clientId", a.updateInboundClient) g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic) g.POST("/resetAllTraffics", a.resetAllTraffics) g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics) g.POST("/delDepletedClients/:id", a.delDepletedClients) g.POST("/import", a.importInbound) g.POST("/onlines", a.onlines) g.POST("/lastOnline", a.lastOnline) g.POST("/updateClientTraffic/:email", a.updateClientTraffic) g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail) g.POST("/batchUpdateClients", a.batchUpdateInboundClients) } // getInbounds retrieves the list of inbounds for the logged-in user. func (a *InboundController) getInbounds(c *gin.Context) { user := session.GetLoginUser(c) inbounds, err := a.inboundService.GetInbounds(user.Id) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err) return } jsonObj(c, inbounds, nil) } // getInbound retrieves a specific inbound by its ID. func (a *InboundController) getInbound(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { jsonMsg(c, I18nWeb(c, "get"), err) return } user := session.GetLoginUser(c) inbound, err := a.inboundService.GetInboundForUser(user.Id, user.Role == "admin", id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), errors.New("inbound not found")) return } jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err) return } jsonObj(c, inbound, nil) } // getClientTraffics retrieves client traffic information by email. func (a *InboundController) getClientTraffics(c *gin.Context) { email := c.Param("email") clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.trafficGetError"), err) return } jsonObj(c, clientTraffics, nil) } // getClientTrafficsById retrieves client traffic information by inbound ID. func (a *InboundController) getClientTrafficsById(c *gin.Context) { id := c.Param("id") clientTraffics, err := a.inboundService.GetClientTrafficByID(id) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.trafficGetError"), err) return } jsonObj(c, clientTraffics, nil) } // addInbound creates a new inbound configuration. func (a *InboundController) addInbound(c *gin.Context) { inbound := &model.Inbound{} err := c.ShouldBind(inbound) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), err) return } user := session.GetLoginUser(c) inbound.UserId = user.Id if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) } else { inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port) } inbound, needRestart, err := a.inboundService.AddInbound(inbound) if err != nil { jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) return } jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, nil) if needRestart { a.xrayService.SetToNeedRestart() } // Broadcast inbounds update via WebSocket inbounds, _ := a.inboundService.GetInbounds(user.Id) websocket.BroadcastInbounds(inbounds) } // delInbound deletes an inbound configuration by its ID. func (a *InboundController) delInbound(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), err) return } user := session.GetLoginUser(c) needRestart, err := a.inboundService.DelInboundForUser(user.Id, user.Role == "admin", id) if err != nil { jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) return } jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, nil) if needRestart { a.xrayService.SetToNeedRestart() } // Broadcast inbounds update via WebSocket inbounds, _ := a.inboundService.GetInbounds(user.Id) websocket.BroadcastInbounds(inbounds) } // updateInbound updates an existing inbound configuration. func (a *InboundController) updateInbound(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err) return } inbound := &model.Inbound{ Id: id, } err = c.ShouldBind(inbound) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err) return } user := session.GetLoginUser(c) inbound, needRestart, err := a.inboundService.UpdateInboundForUser(user.Id, user.Role == "admin", inbound) if err != nil { jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) return } jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, nil) if needRestart { a.xrayService.SetToNeedRestart() } // Broadcast inbounds update via WebSocket inbounds, _ := a.inboundService.GetInbounds(user.Id) websocket.BroadcastInbounds(inbounds) } // getClientIps retrieves the IP addresses associated with a client by email. func (a *InboundController) getClientIps(c *gin.Context) { email := c.Param("email") ips, err := a.inboundService.GetInboundClientIps(email) if err != nil || ips == "" { jsonObj(c, "No IP Record", nil) return } // Prefer returning a normalized string list for consistent UI rendering 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 } // If parsing fails, return as string jsonObj(c, ips, nil) } // clearClientIps clears the IP addresses for a client by email. func (a *InboundController) clearClientIps(c *gin.Context) { email := c.Param("email") err := a.inboundService.ClearClientIps(email) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.updateSuccess"), err) return } jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.logCleanSuccess"), nil) } // addInboundClient adds a new client to an existing inbound. func (a *InboundController) addInboundClient(c *gin.Context) { data := &model.Inbound{} err := c.ShouldBind(data) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err) return } user := session.GetLoginUser(c) needRestart, err := a.inboundService.AddInboundClientForUser(user.Id, user.Role == "admin", data) if err != nil { jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) return } jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientAddSuccess"), nil) if needRestart { a.xrayService.SetToNeedRestart() } } // delInboundClient deletes a client from an inbound by inbound ID and client ID. func (a *InboundController) delInboundClient(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err) return } clientId := c.Param("clientId") user := session.GetLoginUser(c) needRestart, err := a.inboundService.DelInboundClientForUser(user.Id, user.Role == "admin", id, clientId) if err != nil { jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) return } jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientDeleteSuccess"), nil) if needRestart { a.xrayService.SetToNeedRestart() } } // updateInboundClient updates a client's configuration in an inbound. func (a *InboundController) updateInboundClient(c *gin.Context) { clientId := c.Param("clientId") inbound := &model.Inbound{} err := c.ShouldBind(inbound) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err) return } user := session.GetLoginUser(c) needRestart, err := a.inboundService.UpdateInboundClientForUser(user.Id, user.Role == "admin", inbound, clientId) if err != nil { jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) return } jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil) if needRestart { a.xrayService.SetToNeedRestart() } } // resetClientTraffic resets the traffic counter for a specific client in an inbound. func (a *InboundController) resetClientTraffic(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err) return } email := c.Param("email") user := session.GetLoginUser(c) needRestart, err := a.inboundService.ResetClientTrafficForUser(user.Id, user.Role == "admin", id, 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() } } // resetAllTraffics resets all traffic counters across all inbounds. func (a *InboundController) resetAllTraffics(c *gin.Context) { err := a.inboundService.ResetAllTraffics() if err != nil { jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) return } else { a.xrayService.SetToNeedRestart() } jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllTrafficSuccess"), nil) } // resetAllClientTraffics resets traffic counters for all clients in a specific inbound. func (a *InboundController) resetAllClientTraffics(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err) return } user := session.GetLoginUser(c) err = a.inboundService.ResetAllClientTrafficsForUser(user.Id, user.Role == "admin", id) if err != nil { jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) return } else { a.xrayService.SetToNeedRestart() } jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllClientTrafficSuccess"), nil) } // importInbound imports an inbound configuration from provided data. func (a *InboundController) importInbound(c *gin.Context) { inbound := &model.Inbound{} err := json.Unmarshal([]byte(c.PostForm("data")), inbound) if err != nil { jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) return } user := session.GetLoginUser(c) inbound.Id = 0 inbound.UserId = user.Id if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) } else { inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port) } for index := range inbound.ClientStats { inbound.ClientStats[index].Id = 0 inbound.ClientStats[index].Enable = true } needRestart := false inbound, needRestart, err = a.inboundService.AddInbound(inbound) jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, err) if err == nil && needRestart { a.xrayService.SetToNeedRestart() } } // delDepletedClients deletes clients in an inbound who have exhausted their traffic limits. func (a *InboundController) delDepletedClients(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err) return } user := session.GetLoginUser(c) err = a.inboundService.DelDepletedClientsForUser(user.Id, user.Role == "admin", id) if err != nil { jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) return } jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.delDepletedClientsSuccess"), nil) } // onlines retrieves the list of currently online clients. func (a *InboundController) onlines(c *gin.Context) { jsonObj(c, a.inboundService.GetOnlineClients(), nil) } // lastOnline retrieves the last online timestamps for clients. func (a *InboundController) lastOnline(c *gin.Context) { data, err := a.inboundService.GetClientsLastOnline() jsonObj(c, data, err) } // updateClientTraffic updates the traffic statistics for a client by email. func (a *InboundController) updateClientTraffic(c *gin.Context) { email := c.Param("email") // Define the request structure for traffic update type TrafficUpdateRequest struct { Upload int64 `json:"upload"` Download int64 `json:"download"` } var request TrafficUpdateRequest err := c.ShouldBindJSON(&request) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err) return } err = a.inboundService.UpdateClientTrafficByEmail(email, request.Upload, request.Download) if err != nil { jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) return } jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil) } // delInboundClientByEmail deletes a client from an inbound by email address. func (a *InboundController) delInboundClientByEmail(c *gin.Context) { inboundId, err := strconv.Atoi(c.Param("id")) if err != nil { jsonMsg(c, "Invalid inbound ID", err) return } email := c.Param("email") user := session.GetLoginUser(c) needRestart, err := a.inboundService.DelInboundClientByEmailForUser(user.Id, user.Role == "admin", inboundId, email) if err != nil { jsonMsg(c, "Failed to delete client by email", err) return } jsonMsg(c, "Client deleted successfully", nil) if needRestart { a.xrayService.SetToNeedRestart() } } // batchUpdateInboundClients updates multiple clients in an inbound with the same field changes. func (a *InboundController) batchUpdateInboundClients(c *gin.Context) { var request struct { InboundID int `json:"inboundId"` ClientIDs []string `json:"clientIds"` UpdateFields string `json:"updateFields"` } if err := c.ShouldBind(&request); err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err) return } user := session.GetLoginUser(c) needRestart, err := a.inboundService.BatchUpdateInboundClientsForUser( user.Id, user.Role == "admin", request.InboundID, request.ClientIDs, request.UpdateFields, ) if err != nil { jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) return } jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil) if needRestart { a.xrayService.SetToNeedRestart() } } // getUserInfo returns client traffic information for the logged-in user. func (a *InboundController) getUserInfo(c *gin.Context) { user := session.GetLoginUser(c) traffic, err := a.inboundService.GetClientTrafficByEmail(user.Username) if err != nil { jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.trafficGetError"), err) return } jsonObj(c, traffic, nil) } // getUserSubscriptions returns subscription URLs for the logged-in user. func (a *InboundController) getUserSubscriptions(c *gin.Context) { user := session.GetLoginUser(c) traffic, err := a.inboundService.GetClientTrafficByEmail(user.Username) if err != nil || traffic == nil { jsonObj(c, gin.H{"subClashEnable": false}, nil) return } subId := traffic.SubId if subId == "" { jsonObj(c, gin.H{"subClashEnable": false}, nil) return } settingsAny, err := a.settingService.GetDefaultSettings(c.Request.Host) if err != nil { jsonObj(c, gin.H{"subClashEnable": false}, nil) return } settings, ok := settingsAny.(map[string]any) if !ok { jsonObj(c, gin.H{"subClashEnable": false}, nil) return } subEnable := false if v, ok := settings["subEnable"]; ok { if b, ok2 := v.(bool); ok2 { subEnable = b } } subUrl := "" if subEnable { if uri, ok := settings["subURI"]; ok { if s, ok2 := uri.(string); ok2 && s != "" { subUrl = s + subId } } } subClashEnable := false if v, ok := settings["subClashEnable"]; ok { if b, ok2 := v.(bool); ok2 { subClashEnable = b } } subClashUrl := "" if subClashEnable { if uri, ok := settings["subClashURI"]; ok { if s, ok2 := uri.(string); ok2 && s != "" { subClashUrl = s + subId } } } jsonObj(c, gin.H{ "subId": subId, "subEnable": subEnable, "subUrl": subUrl, "subClashEnable": subClashEnable, "subClashUrl": subClashUrl, }, nil) }