client_external_link.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. package service
  2. import (
  3. "net/url"
  4. "strings"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  7. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  8. "github.com/mhsanaei/3x-ui/v3/internal/util/link"
  9. "gorm.io/gorm"
  10. )
  11. // ExternalLinkInput is one row from the client form's Links tab.
  12. type ExternalLinkInput struct {
  13. Kind string `json:"kind"`
  14. Value string `json:"value"`
  15. Remark string `json:"remark"`
  16. Enable *bool `json:"enable"`
  17. ExpiryTime int64 `json:"expiryTime"`
  18. NamePrefix string `json:"namePrefix"`
  19. }
  20. func (s *ClientService) GetExternalLinksForRecord(id int) ([]model.ClientExternalLink, error) {
  21. var rows []model.ClientExternalLink
  22. if err := database.GetDB().
  23. Where("client_id = ?", id).
  24. Order("sort_index ASC, id ASC").
  25. Find(&rows).Error; err != nil {
  26. return nil, err
  27. }
  28. return rows, nil
  29. }
  30. // normalizeExternalLinks validates and orders the incoming rows. A "link" must
  31. // parse to a supported share-link scheme; a "subscription" must be an http(s)
  32. // URL. Blank values are dropped; an invalid value is a hard error so the
  33. // operator gets immediate feedback instead of a silently missing config.
  34. func normalizeExternalLinks(inputs []ExternalLinkInput) ([]model.ClientExternalLink, error) {
  35. out := make([]model.ClientExternalLink, 0, len(inputs))
  36. for _, in := range inputs {
  37. value := strings.TrimSpace(in.Value)
  38. if value == "" {
  39. continue
  40. }
  41. kind := strings.TrimSpace(in.Kind)
  42. switch kind {
  43. case model.ExternalLinkKindSubscription:
  44. if !isHTTPURL(value) {
  45. return nil, common.NewError("external subscription must be an http(s) URL: " + value)
  46. }
  47. case model.ExternalLinkKindLink, "":
  48. kind = model.ExternalLinkKindLink
  49. if _, err := link.ParseLink(value); err != nil {
  50. return nil, common.NewError("unsupported or invalid share link: " + value)
  51. }
  52. default:
  53. return nil, common.NewError("unknown external link kind: " + kind)
  54. }
  55. if in.ExpiryTime < 0 {
  56. return nil, common.NewError("external link expiryTime must be 0 (never) or a future unix millisecond timestamp: " + value)
  57. }
  58. enable := true
  59. if in.Enable != nil {
  60. enable = *in.Enable
  61. }
  62. out = append(out, model.ClientExternalLink{
  63. Kind: kind,
  64. Value: value,
  65. Remark: strings.TrimSpace(in.Remark),
  66. Enable: &enable,
  67. ExpiryTime: in.ExpiryTime,
  68. NamePrefix: in.NamePrefix,
  69. SortIndex: len(out),
  70. })
  71. }
  72. return out, nil
  73. }
  74. func isHTTPURL(s string) bool {
  75. u, err := url.Parse(s)
  76. return err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != ""
  77. }
  78. // SetExternalLinksForRecord replaces a client's entire external-link set.
  79. func (s *ClientService) SetExternalLinksForRecord(id int, inputs []ExternalLinkInput) error {
  80. rows, err := normalizeExternalLinks(inputs)
  81. if err != nil {
  82. return err
  83. }
  84. db := database.GetDB()
  85. return db.Transaction(func(tx *gorm.DB) error {
  86. var existing []model.ClientExternalLink
  87. if err := tx.Where("client_id = ?", id).Find(&existing).Error; err != nil {
  88. return err
  89. }
  90. byKindValue := make(map[string]model.ClientExternalLink, len(existing))
  91. for _, row := range existing {
  92. key := row.Kind + "\x00" + row.Value
  93. if _, ok := byKindValue[key]; !ok {
  94. byKindValue[key] = row
  95. }
  96. }
  97. if err := tx.Where("client_id = ?", id).Delete(&model.ClientExternalLink{}).Error; err != nil {
  98. return err
  99. }
  100. for i := range rows {
  101. if old, ok := byKindValue[rows[i].Kind+"\x00"+rows[i].Value]; ok {
  102. rows[i].LastFetchAt = old.LastFetchAt
  103. rows[i].LastFetchError = old.LastFetchError
  104. }
  105. rows[i].ClientId = id
  106. if err := tx.Create(&rows[i]).Error; err != nil {
  107. return err
  108. }
  109. }
  110. return nil
  111. })
  112. }
  113. func (s *ClientService) SetExternalLinksByEmail(email string, inputs []ExternalLinkInput) error {
  114. if strings.TrimSpace(email) == "" {
  115. return common.NewError("client email is required")
  116. }
  117. rec, err := s.GetRecordByEmail(nil, email)
  118. if err != nil {
  119. return err
  120. }
  121. return s.SetExternalLinksForRecord(rec.Id, inputs)
  122. }