mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-07 21:55:01 +03:00
feat(clients): bind host rules to individual clients in subscriptions (#6620)
Host rules applied per inbound only, so every client of an inbound got the same subscription endpoints (#6618). A client can now be bound to one or more host rules, and its raw, JSON and Clash subscriptions render only those hosts, in either subscription order; an unbound client is unchanged. A binding whose hosts are deleted, disabled or excluded from the format fails closed: the client gets no links there but stays in traffic accounting. Host edits keep row ids, so bindings survive them; nil leaves a stored binding alone and an empty list clears it.
This commit is contained in:
@@ -202,6 +202,17 @@ func (s *HostService) GetHosts() ([]*entity.HostGroup, error) {
|
||||
return groupHosts(hosts), nil
|
||||
}
|
||||
|
||||
// GetRawHosts returns every host row with its numeric id, so callers that
|
||||
// bind by host id (per-client host rule overrides) can list candidates.
|
||||
func (s *HostService) GetRawHosts() ([]*model.Host, error) {
|
||||
var hosts []*model.Host
|
||||
err := database.GetDB().Order("sort_order asc, id asc").Find(&hosts).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
func (s *HostService) GetHostsByInbound(inboundId int) ([]*entity.HostGroup, error) {
|
||||
var groupIds []string
|
||||
if err := database.GetDB().Model(&model.Host{}).Where("inbound_id = ?", inboundId).Distinct().Pluck("group_id", &groupIds).Error; err != nil {
|
||||
@@ -262,31 +273,90 @@ func (s *HostService) UpdateHostGroup(groupId string, req *entity.HostGroup) ([]
|
||||
if err := validateFinalMaskJSONSalamander(req.FinalMask); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
created := buildHostRows(groupId, req)
|
||||
desired := buildHostRows(groupId, req)
|
||||
|
||||
err := database.GetDB().Transaction(func(tx *gorm.DB) error {
|
||||
var count int64
|
||||
if err := tx.Model(&model.Host{}).Where("group_id = ?", groupId).Count(&count).Error; err != nil {
|
||||
var existing []*model.Host
|
||||
if err := tx.Where("group_id = ?", groupId).Order("id asc").Find(&existing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
if len(existing) == 0 {
|
||||
return common.NewError("host not found")
|
||||
}
|
||||
if err := validateInboundsExist(tx, req.InboundIds); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("group_id = ?", groupId).Delete(&model.Host{}).Error; err != nil {
|
||||
return err
|
||||
// Exact (inbound, address, port) matches reuse ids so edits keep bindings; a second pass pairs
|
||||
// same-inbound leftovers with edits, and rows still unclaimed are deleted.
|
||||
type hostKey struct {
|
||||
inbound int
|
||||
addr string
|
||||
port int
|
||||
}
|
||||
if len(created) > 0 {
|
||||
return tx.Create(&created).Error
|
||||
byKey := make(map[hostKey][]*model.Host)
|
||||
for _, h := range existing {
|
||||
k := hostKey{h.InboundId, h.Address, h.Port}
|
||||
byKey[k] = append(byKey[k], h)
|
||||
}
|
||||
var toInsert, toUpdate []*model.Host
|
||||
var unmatched []*model.Host
|
||||
matched := make(map[int]struct{}, len(desired))
|
||||
for _, d := range desired {
|
||||
k := hostKey{d.InboundId, d.Address, d.Port}
|
||||
if q := byKey[k]; len(q) > 0 {
|
||||
d.Id = q[0].Id
|
||||
d.CreatedAt = q[0].CreatedAt
|
||||
byKey[k] = q[1:]
|
||||
toUpdate = append(toUpdate, d)
|
||||
matched[d.Id] = struct{}{}
|
||||
} else {
|
||||
unmatched = append(unmatched, d)
|
||||
}
|
||||
}
|
||||
leftovers := make(map[int][]*model.Host)
|
||||
for _, h := range existing {
|
||||
if _, ok := matched[h.Id]; !ok {
|
||||
leftovers[h.InboundId] = append(leftovers[h.InboundId], h)
|
||||
}
|
||||
}
|
||||
for _, d := range unmatched {
|
||||
if q := leftovers[d.InboundId]; len(q) > 0 {
|
||||
d.Id = q[0].Id
|
||||
d.CreatedAt = q[0].CreatedAt
|
||||
leftovers[d.InboundId] = q[1:]
|
||||
toUpdate = append(toUpdate, d)
|
||||
matched[d.Id] = struct{}{}
|
||||
} else {
|
||||
toInsert = append(toInsert, d)
|
||||
}
|
||||
}
|
||||
var toDelete []int
|
||||
for _, h := range existing {
|
||||
if _, ok := matched[h.Id]; !ok {
|
||||
toDelete = append(toDelete, h.Id)
|
||||
}
|
||||
}
|
||||
if len(toDelete) > 0 {
|
||||
if err := tx.Where("id IN ?", toDelete).Delete(&model.Host{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, u := range toUpdate {
|
||||
if err := tx.Save(u).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(toInsert) > 0 {
|
||||
if err := tx.Create(&toInsert).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return created, nil
|
||||
return desired, nil
|
||||
}
|
||||
|
||||
func (s *HostService) DeleteHostGroup(groupId string) error {
|
||||
|
||||
Reference in New Issue
Block a user