client_portable.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. package service
  2. import (
  3. "strings"
  4. "time"
  5. "github.com/google/uuid"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  9. "gorm.io/gorm"
  10. )
  11. // ExportAll returns every client in the same {client, inboundIds} shape that
  12. // /add and /bulkCreate accept, so an exported file round-trips straight back
  13. // through Import. Clients with no inbound attachment are included with an empty
  14. // inboundIds list so an export taken before DeleteOrphans can restore them.
  15. func (s *ClientService) ExportAll() ([]ClientCreatePayload, error) {
  16. db := database.GetDB()
  17. var rows []model.ClientRecord
  18. if err := db.Order("id ASC").Find(&rows).Error; err != nil {
  19. return nil, err
  20. }
  21. out := make([]ClientCreatePayload, 0, len(rows))
  22. if len(rows) == 0 {
  23. return out, nil
  24. }
  25. ids := make([]int, 0, len(rows))
  26. for i := range rows {
  27. ids = append(ids, rows[i].Id)
  28. }
  29. attachments := make(map[int][]int, len(rows))
  30. for _, batch := range chunkInts(ids, sqlInChunk) {
  31. var links []model.ClientInbound
  32. if err := db.Where("client_id IN ?", batch).Order("inbound_id ASC").Find(&links).Error; err != nil {
  33. return nil, err
  34. }
  35. for _, l := range links {
  36. attachments[l.ClientId] = append(attachments[l.ClientId], l.InboundId)
  37. }
  38. }
  39. for i := range rows {
  40. client := rows[i].ToClient()
  41. // The per-inbound flow_override is the reliable flow for multi-inbound
  42. // clients; the canonical column can be left stale by SyncInbound (#4792).
  43. if flow, err := s.EffectiveFlow(db, rows[i].Id); err == nil && flow != "" {
  44. client.Flow = flow
  45. }
  46. out = append(out, ClientCreatePayload{
  47. Client: *client,
  48. InboundIds: attachments[rows[i].Id],
  49. LimitHwid: rows[i].LimitHwid,
  50. })
  51. }
  52. return out, nil
  53. }
  54. // ImportClients recreates clients from an exported list. Items that carry
  55. // inboundIds go through the normal BulkCreate path (added to every inbound and
  56. // pushed to xray); items with no inboundIds are restored as bare records so an
  57. // orphan-inclusive export round-trips. Existing emails are never overwritten —
  58. // they are reported in Skipped. The boolean reports whether xray needs a restart.
  59. func (s *ClientService) ImportClients(inboundSvc *InboundService, items []ClientCreatePayload) (BulkCreateResult, bool, error) {
  60. result := BulkCreateResult{}
  61. if len(items) == 0 {
  62. return result, false, nil
  63. }
  64. attached := make([]ClientCreatePayload, 0, len(items))
  65. orphans := make([]ClientCreatePayload, 0)
  66. for i := range items {
  67. if len(items[i].InboundIds) > 0 {
  68. attached = append(attached, items[i])
  69. } else {
  70. orphans = append(orphans, items[i])
  71. }
  72. }
  73. skip := func(email, reason string) {
  74. if strings.TrimSpace(email) == "" {
  75. email = "(missing email)"
  76. }
  77. result.Skipped = append(result.Skipped, BulkCreateReport{Email: email, Reason: reason})
  78. }
  79. needRestart := false
  80. if len(attached) > 0 {
  81. sub, nr, err := s.BulkCreate(inboundSvc, attached)
  82. if err != nil {
  83. return result, needRestart, err
  84. }
  85. needRestart = needRestart || nr
  86. result.Created += sub.Created
  87. result.Skipped = append(result.Skipped, sub.Skipped...)
  88. }
  89. db := database.GetDB()
  90. for i := range orphans {
  91. client := orphans[i].Client
  92. email := strings.TrimSpace(client.Email)
  93. if email == "" {
  94. skip("", "client email is required")
  95. continue
  96. }
  97. if verr := validateClientEmail(email); verr != nil {
  98. skip(email, verr.Error())
  99. continue
  100. }
  101. if verr := validateClientSubID(client.SubID); verr != nil {
  102. skip(email, verr.Error())
  103. continue
  104. }
  105. if verr := validateClientResetDay(client.ResetDay); verr != nil {
  106. skip(email, verr.Error())
  107. continue
  108. }
  109. if verr := validateClientResetMax(client.ResetMax); verr != nil {
  110. skip(email, verr.Error())
  111. continue
  112. }
  113. if verr := validateClientTrafficReset(client.TrafficReset, client.TrafficResetDay); verr != nil {
  114. skip(email, verr.Error())
  115. continue
  116. }
  117. // An existing record (in the DB or just created from the attached set
  118. // above) always wins — import never clobbers a live client.
  119. var taken int64
  120. if err := db.Model(&model.ClientRecord{}).Where("email = ?", email).Count(&taken).Error; err != nil {
  121. return result, needRestart, err
  122. }
  123. if taken > 0 {
  124. skip(email, "email already in use: "+email)
  125. continue
  126. }
  127. client.Email = email
  128. if client.SubID == "" {
  129. client.SubID = uuid.NewString()
  130. }
  131. if client.SubID != "" {
  132. var subTaken int64
  133. if err := db.Model(&model.ClientRecord{}).
  134. Where("sub_id = ? AND email <> ?", client.SubID, email).
  135. Count(&subTaken).Error; err != nil {
  136. return result, needRestart, err
  137. }
  138. if subTaken > 0 {
  139. skip(email, "subId already in use: "+client.SubID)
  140. continue
  141. }
  142. }
  143. // Preserve exported enable so a disabled orphan stays disabled (#6478).
  144. now := time.Now().UnixMilli()
  145. if client.CreatedAt == 0 {
  146. client.CreatedAt = now
  147. }
  148. client.UpdatedAt = now
  149. rec := client.ToRecord()
  150. rec.LimitHwid = orphans[i].LimitHwid
  151. if err := db.Create(rec).Error; err != nil {
  152. skip(email, err.Error())
  153. continue
  154. }
  155. // gorm default:true drops enable=false on Create — restate (#6478).
  156. if !client.Enable {
  157. if err := db.Model(&model.ClientRecord{}).Where("id = ?", rec.Id).
  158. UpdateColumn("enable", false).Error; err != nil {
  159. return result, needRestart, err
  160. }
  161. }
  162. result.Created++
  163. }
  164. return result, needRestart, nil
  165. }
  166. // DeleteOrphans removes every client that is not attached to any inbound,
  167. // together with its traffic rows, IP log, and external links. It mirrors the
  168. // cleanup the single-client Delete performs, batched into one transaction.
  169. // Returns the number of clients deleted.
  170. func (s *ClientService) DeleteOrphans() (int, error) {
  171. db := database.GetDB()
  172. sub := database.GetDB().Table("client_inbounds").Select("client_id")
  173. var rows []model.ClientRecord
  174. if err := db.Where("id NOT IN (?)", sub).Order("id ASC").Find(&rows).Error; err != nil {
  175. return 0, err
  176. }
  177. if len(rows) == 0 {
  178. return 0, nil
  179. }
  180. ids := make([]int, 0, len(rows))
  181. emails := make([]string, 0, len(rows))
  182. subIDs := make([]string, 0, len(rows))
  183. for i := range rows {
  184. ids = append(ids, rows[i].Id)
  185. if rows[i].Email != "" {
  186. emails = append(emails, rows[i].Email)
  187. }
  188. subIDs = append(subIDs, rows[i].SubID)
  189. }
  190. tombstoneClientEmails(emails)
  191. if err := runSerializedTx(func(tx *gorm.DB) error {
  192. if e := adjustGroupBaselinesForRemovedTraffic(tx, emails); e != nil {
  193. return e
  194. }
  195. if e := clearClientHwidsBySubIDTx(tx, subIDs...); e != nil {
  196. return e
  197. }
  198. for _, batch := range chunkInts(ids, sqlInChunk) {
  199. if e := tx.Where("client_id IN ?", batch).Delete(&model.ClientInbound{}).Error; e != nil {
  200. return e
  201. }
  202. if e := tx.Where("client_id IN ?", batch).Delete(&model.ClientExternalLink{}).Error; e != nil {
  203. return e
  204. }
  205. }
  206. if len(emails) > 0 {
  207. for _, batch := range chunkStrings(emails, sqlInChunk) {
  208. if e := tx.Where("email IN ?", batch).Delete(&xray.ClientTraffic{}).Error; e != nil {
  209. return e
  210. }
  211. if e := tx.Where("client_email IN ?", batch).Delete(&model.InboundClientIps{}).Error; e != nil {
  212. return e
  213. }
  214. }
  215. if e := clearGlobalTraffic(tx, emails...); e != nil {
  216. return e
  217. }
  218. }
  219. for _, batch := range chunkInts(ids, sqlInChunk) {
  220. if e := tx.Where("id IN ?", batch).Delete(&model.ClientRecord{}).Error; e != nil {
  221. return e
  222. }
  223. }
  224. return nil
  225. }); err != nil {
  226. return 0, err
  227. }
  228. return len(ids), nil
  229. }