Release 3X-UI for Docker / build (push) Canceled after 0s
Release 3X-UI / build (386) (push) Canceled after 0s
Release 3X-UI / build (amd64) (push) Canceled after 0s
Release 3X-UI / build (arm64) (push) Canceled after 0s
Release 3X-UI / build (armv5) (push) Canceled after 0s
Release 3X-UI / build (armv6) (push) Canceled after 0s
Release 3X-UI / build (armv7) (push) Canceled after 0s
Release 3X-UI / build (s390x) (push) Canceled after 0s
Release 3X-UI / Build for Windows (amd64) (push) Canceled after 0s
Release 3X-UI / Publish rolling dev release (push) Canceled after 0s
132 lines
3.9 KiB
Go
132 lines
3.9 KiB
Go
package service
|
|
|
|
import (
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/link"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ExternalLinkInput is one row from the client form's Links tab.
|
|
type ExternalLinkInput struct {
|
|
Kind string `json:"kind"`
|
|
Value string `json:"value"`
|
|
Remark string `json:"remark"`
|
|
Enable *bool `json:"enable"`
|
|
ExpiryTime int64 `json:"expiryTime"`
|
|
NamePrefix string `json:"namePrefix"`
|
|
}
|
|
|
|
func (s *ClientService) GetExternalLinksForRecord(id int) ([]model.ClientExternalLink, error) {
|
|
var rows []model.ClientExternalLink
|
|
if err := database.GetDB().
|
|
Where("client_id = ?", id).
|
|
Order("sort_index ASC, id ASC").
|
|
Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
// normalizeExternalLinks validates and orders the incoming rows. A "link" must
|
|
// parse to a supported share-link scheme; a "subscription" must be an http(s)
|
|
// URL. Blank values are dropped; an invalid value is a hard error so the
|
|
// operator gets immediate feedback instead of a silently missing config.
|
|
func normalizeExternalLinks(inputs []ExternalLinkInput) ([]model.ClientExternalLink, error) {
|
|
out := make([]model.ClientExternalLink, 0, len(inputs))
|
|
for _, in := range inputs {
|
|
value := strings.TrimSpace(in.Value)
|
|
if value == "" {
|
|
continue
|
|
}
|
|
kind := strings.TrimSpace(in.Kind)
|
|
switch kind {
|
|
case model.ExternalLinkKindSubscription:
|
|
if !isHTTPURL(value) {
|
|
return nil, common.NewError("external subscription must be an http(s) URL: " + value)
|
|
}
|
|
case model.ExternalLinkKindLink, "":
|
|
kind = model.ExternalLinkKindLink
|
|
if _, err := link.ParseLink(value); err != nil {
|
|
return nil, common.NewError("unsupported or invalid share link: " + value)
|
|
}
|
|
default:
|
|
return nil, common.NewError("unknown external link kind: " + kind)
|
|
}
|
|
if in.ExpiryTime < 0 {
|
|
return nil, common.NewError("external link expiryTime must be 0 (never) or a future unix millisecond timestamp: " + value)
|
|
}
|
|
enable := true
|
|
if in.Enable != nil {
|
|
enable = *in.Enable
|
|
}
|
|
out = append(out, model.ClientExternalLink{
|
|
Kind: kind,
|
|
Value: value,
|
|
Remark: strings.TrimSpace(in.Remark),
|
|
Enable: &enable,
|
|
ExpiryTime: in.ExpiryTime,
|
|
NamePrefix: in.NamePrefix,
|
|
SortIndex: len(out),
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func isHTTPURL(s string) bool {
|
|
u, err := url.Parse(s)
|
|
return err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != ""
|
|
}
|
|
|
|
// SetExternalLinksForRecord replaces a client's entire external-link set.
|
|
func (s *ClientService) SetExternalLinksForRecord(id int, inputs []ExternalLinkInput) error {
|
|
rows, err := normalizeExternalLinks(inputs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
db := database.GetDB()
|
|
return db.Transaction(func(tx *gorm.DB) error {
|
|
var existing []model.ClientExternalLink
|
|
if err := tx.Where("client_id = ?", id).Find(&existing).Error; err != nil {
|
|
return err
|
|
}
|
|
byKindValue := make(map[string]model.ClientExternalLink, len(existing))
|
|
for _, row := range existing {
|
|
key := row.Kind + "\x00" + row.Value
|
|
if _, ok := byKindValue[key]; !ok {
|
|
byKindValue[key] = row
|
|
}
|
|
}
|
|
if err := tx.Where("client_id = ?", id).Delete(&model.ClientExternalLink{}).Error; err != nil {
|
|
return err
|
|
}
|
|
for i := range rows {
|
|
if old, ok := byKindValue[rows[i].Kind+"\x00"+rows[i].Value]; ok {
|
|
rows[i].LastFetchAt = old.LastFetchAt
|
|
rows[i].LastFetchError = old.LastFetchError
|
|
}
|
|
rows[i].ClientId = id
|
|
if err := tx.Create(&rows[i]).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (s *ClientService) SetExternalLinksByEmail(email string, inputs []ExternalLinkInput) error {
|
|
if strings.TrimSpace(email) == "" {
|
|
return common.NewError("client email is required")
|
|
}
|
|
rec, err := s.GetRecordByEmail(nil, email)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.SetExternalLinksForRecord(rec.Id, inputs)
|
|
}
|