Files
3x-ui/internal/web/service/inbound_settings_clients.go
MHSanaei 15d82a5e47 fix(inbounds): accept v2.x client fields on inbound import
Panels up to v2.x stored a client's tgId as a string, "" when unset. The
startup migration heals copies already in the database, but an exported
inbound imported into a current panel goes straight to AddInbound, whose
typed client parse failed with "cannot unmarshal string into Go struct
field .0.tgId of type int64", so every such import was refused.

AddInbound now runs the legacy normalizer the clients-table seeder already
used (moved from database to model so both share it) over the incoming
settings before parsing them. String numbers become integers and empty ones
are dropped, and the settings are stored in that shape. The same applies to
/inbounds/add callers still sending the old types.

The seeder change is a pure move; the PostgreSQL lane was not run (no
Docker on this host) and is left to CI.

Closes #6663
2026-09-27 18:28:07 +02:00

76 lines
2.0 KiB
Go

package service
import (
"bytes"
"encoding/json"
"strings"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
)
func ParseInboundSettingsClients(settings string) ([]model.Client, error) {
trimmed := strings.TrimSpace(settings)
if trimmed == "" || trimmed == "null" {
return nil, common.NewError("inbound settings is empty")
}
var payload struct {
Clients json.RawMessage `json:"clients"`
}
if err := json.Unmarshal([]byte(trimmed), &payload); err != nil {
return nil, err
}
if len(payload.Clients) == 0 || string(payload.Clients) == "null" {
return nil, nil
}
var clients []model.Client
if err := json.Unmarshal(payload.Clients, &clients); err != nil {
return nil, err
}
return clients, nil
}
// settingsEntriesToClients decodes the wire entries a caller has already
// stamped, so a delta carries the persisted created_at / updated_at / subId
// rather than the pre-stamp values the request was parsed into.
func settingsEntriesToClients(entries []any) ([]model.Client, error) {
if len(entries) == 0 {
return nil, nil
}
raw, err := json.Marshal(entries)
if err != nil {
return nil, err
}
var clients []model.Client
if err := json.Unmarshal(raw, &clients); err != nil {
return nil, err
}
return clients, nil
}
// normalizeLegacyClientSettings rewrites settings whose clients still carry the
// string numbers of a v2.x export, so parsing and storage see a single shape.
func normalizeLegacyClientSettings(inbound *model.Inbound) {
dec := json.NewDecoder(bytes.NewReader([]byte(inbound.Settings)))
dec.UseNumber()
var settings map[string]any
if err := dec.Decode(&settings); err != nil {
return
}
clients, _ := settings["clients"].([]any)
changed := false
for _, raw := range clients {
if obj, ok := raw.(map[string]any); ok && model.NormalizeLegacyClientFields(obj) {
changed = true
}
}
if !changed {
return
}
if out, err := json.MarshalIndent(settings, "", " "); err == nil {
inbound.Settings = string(out)
}
}