mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 03:31:48 +00:00
The group page shows traffic counting per group, but the only reset available zeroed every member client's up/down counters (and their quotas) via bulkResetTraffic. Group traffic is a derived sum of client traffic, so zeroing the group display previously required mutating the clients themselves. Add a display-only baseline: ClientGroup gains reset_up/reset_down columns (additive, handled by AutoMigrate). ResetGroupTraffic snapshots the group's current up/down sum into the baseline, and ListGroups now reports max(0, sum - baseline). Client counters are left untouched and no Xray restart is triggered. A new POST /panel/api/clients/groups/ resetTraffic endpoint drives it, creating the client_groups row when the group exists only as a derived label. The groups page action now calls the new endpoint; confirm/success strings updated across all 13 locales to reflect group-only semantics.
173 lines
4.3 KiB
Go
173 lines
4.3 KiB
Go
package controller
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type GroupController struct {
|
|
clientService service.ClientService
|
|
xrayService service.XrayService
|
|
}
|
|
|
|
func NewGroupController(g *gin.RouterGroup) *GroupController {
|
|
a := &GroupController{}
|
|
a.initRouter(g)
|
|
return a
|
|
}
|
|
|
|
func (a *GroupController) initRouter(g *gin.RouterGroup) {
|
|
g.GET("/groups", a.list)
|
|
g.GET("/groups/:name/emails", a.emails)
|
|
g.POST("/groups/create", a.create)
|
|
g.POST("/groups/rename", a.rename)
|
|
g.POST("/groups/delete", a.delete)
|
|
g.POST("/groups/resetTraffic", a.resetTraffic)
|
|
g.POST("/groups/bulkAdd", a.bulkAdd)
|
|
g.POST("/groups/bulkRemove", a.bulkRemove)
|
|
}
|
|
|
|
func (a *GroupController) list(c *gin.Context) {
|
|
rows, err := a.clientService.ListGroups()
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonObj(c, rows, nil)
|
|
}
|
|
|
|
func (a *GroupController) emails(c *gin.Context) {
|
|
name := c.Param("name")
|
|
emails, err := a.clientService.EmailsByGroup(name)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonObj(c, emails, nil)
|
|
}
|
|
|
|
type groupCreateBody struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
func (a *GroupController) create(c *gin.Context) {
|
|
var body groupCreateBody
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
if err := a.clientService.CreateGroup(body.Name); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonObj(c, gin.H{"name": body.Name}, nil)
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
type groupRenameBody struct {
|
|
OldName string `json:"oldName"`
|
|
NewName string `json:"newName"`
|
|
}
|
|
|
|
func (a *GroupController) rename(c *gin.Context) {
|
|
var body groupRenameBody
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
affected, err := a.clientService.RenameGroup(body.OldName, body.NewName)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
a.xrayService.SetToNeedRestart()
|
|
jsonObj(c, gin.H{"affected": affected}, nil)
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
type groupDeleteBody struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
func (a *GroupController) delete(c *gin.Context) {
|
|
var body groupDeleteBody
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
affected, err := a.clientService.DeleteGroup(body.Name)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
a.xrayService.SetToNeedRestart()
|
|
jsonObj(c, gin.H{"affected": affected}, nil)
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
type groupResetTrafficBody struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
func (a *GroupController) resetTraffic(c *gin.Context) {
|
|
var body groupResetTrafficBody
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
if err := a.clientService.ResetGroupTraffic(body.Name); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonObj(c, gin.H{"name": body.Name}, nil)
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
type bulkAddToGroupRequest struct {
|
|
Emails []string `json:"emails"`
|
|
Group string `json:"group"`
|
|
}
|
|
|
|
func (a *GroupController) bulkAdd(c *gin.Context) {
|
|
var req bulkAddToGroupRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Group) == "" {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("group name is required"))
|
|
return
|
|
}
|
|
affected, err := a.clientService.AddToGroup(req.Emails, req.Group)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonObj(c, gin.H{"affected": affected}, nil)
|
|
a.xrayService.SetToNeedRestart()
|
|
notifyClientsChanged()
|
|
}
|
|
|
|
type bulkRemoveFromGroupRequest struct {
|
|
Emails []string `json:"emails"`
|
|
}
|
|
|
|
func (a *GroupController) bulkRemove(c *gin.Context) {
|
|
var req bulkRemoveFromGroupRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
affected, err := a.clientService.RemoveFromGroup(req.Emails)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
jsonObj(c, gin.H{"affected": affected}, nil)
|
|
a.xrayService.SetToNeedRestart()
|
|
notifyClientsChanged()
|
|
}
|