Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions frontend/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3655,6 +3655,60 @@
}
}
},
"/panel/api/inbounds/{id}/subSortIndex": {
"post": {
"tags": [
"Inbounds"
],
"summary": "Set only the subscription sort order. Reads the stored inbound, so a reorder cannot carry a stale client list over a concurrent edit.",
"operationId": "post_panel_api_inbounds_id_subSortIndex",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "Inbound ID.",
"schema": {
"type": "integer"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object"
},
"example": {
"subSortIndex": 2
}
}
}
},
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"msg": {
"type": "string"
},
"obj": {}
}
}
}
}
}
}
}
},
"/panel/api/inbounds/{id}/resetTraffic": {
"post": {
"tags": [
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/pages/api-docs/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,15 @@ export const sections: readonly Section[] = [
],
body: '{\n "enable": false\n}',
},
{
method: 'POST',
path: '/panel/api/inbounds/:id/subSortIndex',
summary: 'Set only the subscription sort order. Reads the stored inbound, so a reorder cannot carry a stale client list over a concurrent edit.',
params: [
{ name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' },
],
body: '{\n "subSortIndex": 2\n}',
},
{
method: 'POST',
path: '/panel/api/inbounds/:id/resetTraffic',
Expand Down
22 changes: 22 additions & 0 deletions internal/web/controller/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/bulkDel", a.bulkDelInbounds)
g.POST("/update/:id", a.updateInbound)
g.POST("/setEnable/:id", a.setInboundEnable)
g.POST("/:id/subSortIndex", a.setInboundSubSortIndex)
g.POST("/:id/resetTraffic", a.resetInboundTraffic)
g.POST("/:id/delAllClients", a.delAllInboundClients)
g.POST("/resetAllTraffics", a.resetAllTraffics)
Expand Down Expand Up @@ -260,6 +261,27 @@ func (a *InboundController) updateInbound(c *gin.Context) {
// settings JSON (every client) — far too heavy for an interactive switch
// on inbounds with thousands of clients. Frontend optimistically updates
// the UI; we just persist + sync xray + nudge other open admin sessions.
func (a *InboundController) setInboundSubSortIndex(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
return
}
type form struct {
SubSortIndex int `json:"subSortIndex" form:"subSortIndex" binding:"required,min=1"`
}
var f form
if err := c.ShouldBind(&f); err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
if err := a.inboundService.SetInboundSubSortIndex(id, f.SubSortIndex); err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), nil)
}

func (a *InboundController) setInboundEnable(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
Expand Down
43 changes: 43 additions & 0 deletions internal/web/service/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,49 @@ func (s *InboundService) GetInboundDetail(id int) (*model.Inbound, error) {
return inbound, nil
}

// SetInboundSubSortIndex changes only the subscription sort order. It reads the
// stored inbound instead of accepting one, so a reorder cannot carry a stale
// settings/client payload over a concurrent edit.
func (s *InboundService) SetInboundSubSortIndex(id int, index int) error {
index = normalizeSubSortIndex(index)
inbound, err := s.GetInbound(id)
if err != nil {
return err
}
if inbound.SubSortIndex == index {
return nil
}

db := database.GetDB()
if err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(model.Inbound{}).Where("id = ?", id).
Update("sub_sort_index", index).Error; err != nil {
return err
}
if inbound.NodeID != nil {
return (&NodeService{}).MarkNodeDirtyTx(tx, *inbound.NodeID)
}
return nil
}); err != nil {
return err
}
inbound.SubSortIndex = index

if inbound.NodeID == nil {
return nil
}
rt, push, _, perr := s.nodePushPlan(inbound)
if perr != nil {
return perr
}
if push {
if err := rt.UpdateInbound(context.Background(), inbound, inbound); err != nil {
logger.Warning("SetInboundSubSortIndex: remote UpdateInbound on", rt.Name(), "failed:", err)
}
}
return nil
}

func (s *InboundService) SetInboundEnable(id int, enable bool) (bool, error) {
inbound, err := s.GetInbound(id)
if err != nil {
Expand Down
38 changes: 38 additions & 0 deletions internal/web/service/inbound_subsort_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package service

import (
"path/filepath"
"testing"

"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)

func TestSetInboundSubSortIndexLeavesSettingsUntouched(t *testing.T) {
if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
t.Fatalf("init db: %v", err)
}
t.Cleanup(func() { _ = database.CloseDB() })

const settings = `{"clients":[{"email":"a@example.test","id":"11111111-1111-1111-1111-111111111111"}]}`
ib := &model.Inbound{UserId: 1, Remark: "r", Port: 21001, Protocol: model.VLESS, Settings: settings, SubSortIndex: 1, Enable: true}
if err := database.GetDB().Create(ib).Error; err != nil {
t.Fatalf("seed: %v", err)
}

svc := InboundService{}
if err := svc.SetInboundSubSortIndex(ib.Id, 7); err != nil {
t.Fatalf("set: %v", err)
}

var got model.Inbound
if err := database.GetDB().First(&got, ib.Id).Error; err != nil {
t.Fatalf("reload: %v", err)
}
if got.SubSortIndex != 7 {
t.Fatalf("subSortIndex = %d, want 7", got.SubSortIndex)
}
if got.Settings != settings {
t.Fatalf("settings were rewritten:\n got %s\nwant %s", got.Settings, settings)
}
}