Files
3x-ui/internal/web/service/client.go
T
Rouzbeh† 694ad6deae feat(sub): add per-client subscription HWID limits (#5802)
* feat(sub): add per-client subscription HWID limits

* fix(sub): address HWID review on shared subId and bulk create

* fix(sub): store HWID devices by sub_id and drop anchor client workaround

* fix(sub): restore UA auto-detect and HTML page routing in subs()

The cherry-pick of the HWID gate onto main's refactored SUBController
had dropped main's UA-based format auto-detection and sub-page handling
from subs(). Restore those branches, slotting enforceHwid after the
HTML page and before format detection so the gate only applies to
machine-readable subscription bodies.

Also adapt tests to main's options-struct constructor and to the
ClientService.Update signature extended with limitHwid.

* fix(frontend): drop axios from HttpUtil.delete

The bulk-delete rework's committed version still referenced axios,
which this file no longer imports, breaking typecheck in CI. Use the
httpRequest wrapper like the other verbs.

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-08-15 16:50:20 +02:00

104 lines
3.4 KiB
Go

// Package service implements the panel's business-logic layer.
//
// ClientService owns the lifecycle of VPN clients: creation, update, deletion,
// attach/detach to inbounds, bulk operations, group membership, traffic resets,
// and the paginated clients listing. Its surface is split across client_*.go
// files by responsibility (see each file's contents); they all belong to the
// same package, so the split is purely organizational. ClientService and
// InboundService are mutually dependent — most ClientService methods take an
// *InboundService and InboundService embeds a ClientService — which is why the
// client code lives in package service rather than a sub-package.
package service
import (
"encoding/json"
"errors"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
)
type ClientWithAttachments struct {
model.ClientRecord
InboundIds []int `json:"inboundIds"`
Traffic *xray.ClientTraffic `json:"traffic,omitempty"`
}
// MarshalJSON is required because model.ClientRecord defines its own
// MarshalJSON. Go promotes the embedded method to the outer struct, so without
// this the encoder would call ClientRecord.MarshalJSON for the whole value and
// silently drop InboundIds and Traffic from the API response.
func (c ClientWithAttachments) MarshalJSON() ([]byte, error) {
rec, err := json.Marshal(c.ClientRecord)
if err != nil {
return nil, err
}
extras := struct {
InboundIds []int `json:"inboundIds"`
Traffic *xray.ClientTraffic `json:"traffic,omitempty"`
}{InboundIds: c.InboundIds, Traffic: c.Traffic}
extra, err := json.Marshal(extras)
if err != nil {
return nil, err
}
if len(rec) < 2 || rec[len(rec)-1] != '}' || len(extra) <= 2 {
return rec, nil
}
const maxMarshalSize = 256 << 20
if len(rec) > maxMarshalSize || len(extra) > maxMarshalSize {
return rec, nil
}
out := make([]byte, 0, len(rec)+len(extra))
out = append(out, rec[:len(rec)-1]...)
if len(rec) > 2 {
out = append(out, ',')
}
out = append(out, extra[1:]...)
return out, nil
}
type ClientService struct{}
// ErrClientNotInInbound is returned (wrapped) when a client cannot be located
// in an inbound's settings during deletion. Deletion treats it as non-fatal so
// the operation stays idempotent and tolerant of pre-existing data drift
// between the clients table and the inbound's settings JSON.
var ErrClientNotInInbound = errors.New("client not found in inbound")
type ClientCreatePayload struct {
Client model.Client `json:"client"`
InboundIds []int `json:"inboundIds"`
LimitHwid int `json:"-"`
}
const sqlInChunk = 400
type clientPayloadWithHwid struct {
model.Client
LimitHwid int `json:"limitHwid"`
}
func (p *ClientCreatePayload) UnmarshalJSON(data []byte) error {
var raw struct {
Client clientPayloadWithHwid `json:"client"`
InboundIds []int `json:"inboundIds"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
p.Client = raw.Client.Client
p.InboundIds = raw.InboundIds
p.LimitHwid = raw.Client.LimitHwid
return nil
}
func (p ClientCreatePayload) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Client clientPayloadWithHwid `json:"client"`
InboundIds []int `json:"inboundIds"`
}{
Client: clientPayloadWithHwid{Client: p.Client, LimitHwid: p.LimitHwid},
InboundIds: p.InboundIds,
})
}