1
0

client_bulk.go 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "strings"
  7. "time"
  8. "github.com/google/uuid"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database"
  10. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  11. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  12. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  13. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  14. "gorm.io/gorm"
  15. )
  16. // BulkAttachResult reports the outcome of a bulk attach across target inbounds.
  17. type BulkAttachResult struct {
  18. Attached []string `json:"attached"`
  19. Skipped []string `json:"skipped"`
  20. Errors []string `json:"errors"`
  21. }
  22. // BulkAttach attaches the given existing clients (by email) to each target inbound,
  23. // reusing their identity (email/UUID/password/subId) and a shared traffic row. It adds
  24. // all clients to a target in a single AddInboundClient call, and reports clients already
  25. // present on a target as skipped.
  26. func (s *ClientService) BulkAttach(inboundSvc *InboundService, emails []string, inboundIds []int) (*BulkAttachResult, bool, error) {
  27. result := &BulkAttachResult{}
  28. if len(emails) == 0 || len(inboundIds) == 0 {
  29. return result, false, nil
  30. }
  31. recordErr := func(format string, args ...any) {
  32. msg := fmt.Sprintf(format, args...)
  33. result.Errors = append(result.Errors, msg)
  34. logger.Warningf("[BulkAttach] %s", msg)
  35. }
  36. records := make([]*model.ClientRecord, 0, len(emails))
  37. seenEmail := make(map[string]struct{}, len(emails))
  38. for _, email := range emails {
  39. if email == "" {
  40. continue
  41. }
  42. key := strings.ToLower(email)
  43. if _, ok := seenEmail[key]; ok {
  44. continue
  45. }
  46. seenEmail[key] = struct{}{}
  47. rec, err := s.GetRecordByEmail(nil, email)
  48. if err != nil {
  49. recordErr("%s: %v", email, err)
  50. continue
  51. }
  52. records = append(records, rec)
  53. }
  54. needRestart := false
  55. // Prepared in order first, as in Create: fillProtocolDefaults mints the
  56. // shared credentials, so only the node pushes below may overlap.
  57. attachIds := make([]int, 0, len(inboundIds))
  58. attachPayloads := make([]string, 0, len(inboundIds))
  59. attachClients := make([][]model.Client, 0, len(inboundIds))
  60. attachAnyTunnel := false
  61. // A repeated id used to be caught by the second pass seeing the client
  62. // already attached; the applies no longer run before the next prep.
  63. seenInbound := make(map[int]struct{}, len(inboundIds))
  64. for _, ibId := range inboundIds {
  65. if _, dup := seenInbound[ibId]; dup {
  66. continue
  67. }
  68. seenInbound[ibId] = struct{}{}
  69. inbound, err := inboundSvc.GetInbound(ibId)
  70. if err != nil {
  71. recordErr("inbound %d: %v", ibId, err)
  72. continue
  73. }
  74. if inbound.Protocol == model.WireGuard || inbound.Protocol == model.AmneziaWG {
  75. attachAnyTunnel = true
  76. }
  77. existingClients, err := inboundSvc.GetClients(inbound)
  78. if err != nil {
  79. recordErr("inbound %d: %v", ibId, err)
  80. continue
  81. }
  82. have := make(map[string]struct{}, len(existingClients))
  83. for _, c := range existingClients {
  84. have[strings.ToLower(c.Email)] = struct{}{}
  85. }
  86. clientsToAdd := make([]model.Client, 0, len(records))
  87. for _, rec := range records {
  88. if _, attached := have[strings.ToLower(rec.Email)]; attached {
  89. result.Skipped = append(result.Skipped, rec.Email)
  90. continue
  91. }
  92. client := *rec.ToClient()
  93. client.UpdatedAt = time.Now().UnixMilli()
  94. if err := s.fillProtocolDefaults(&client, inbound); err != nil {
  95. recordErr("%s -> inbound %d: %v", rec.Email, ibId, err)
  96. continue
  97. }
  98. clientsToAdd = append(clientsToAdd, clientWithInboundFlow(client, inbound))
  99. }
  100. if len(clientsToAdd) == 0 {
  101. continue
  102. }
  103. payload, err := json.Marshal(map[string][]model.Client{"clients": clientsToAdd})
  104. if err != nil {
  105. recordErr("inbound %d: %v", ibId, err)
  106. continue
  107. }
  108. attachIds = append(attachIds, ibId)
  109. attachPayloads = append(attachPayloads, string(payload))
  110. attachClients = append(attachClients, clientsToAdd)
  111. }
  112. attachResults, attachPanics := fanoutInboundResults(attachIds, addFanoutLimit(attachAnyTunnel), func(i int) inboundApplyOutcome {
  113. nr, err := s.AddInboundClient(inboundSvc, &model.Inbound{Id: attachIds[i], Settings: attachPayloads[i]})
  114. return inboundApplyOutcome{needRestart: nr, err: err}
  115. })
  116. for i, out := range attachResults {
  117. err := out.err
  118. if attachPanics[i] != nil {
  119. // The apply may already have committed, so ask for the restart the
  120. // lost return value can no longer report.
  121. needRestart = true
  122. err = attachPanics[i]
  123. }
  124. if err != nil {
  125. recordErr("inbound %d: %v", attachIds[i], err)
  126. continue
  127. }
  128. if out.needRestart {
  129. needRestart = true
  130. }
  131. for _, c := range attachClients[i] {
  132. result.Attached = append(result.Attached, c.Email)
  133. }
  134. }
  135. return result, needRestart, nil
  136. }
  137. // BulkDetachResult reports the outcome of a bulk detach across target inbounds.
  138. type BulkDetachResult struct {
  139. Detached []string `json:"detached"`
  140. Skipped []string `json:"skipped"`
  141. Errors []string `json:"errors"`
  142. }
  143. // BulkDetach detaches the given existing clients (by email) from each target inbound.
  144. // (email, inbound) pairs where the client is not currently attached are silently skipped
  145. // at the inbound level; emails that aren't attached to any of the requested inbounds
  146. // are reported under skipped. ClientRecord rows are kept even when they become orphaned
  147. // (matches single-client detach semantics); callers should use bulkDelete for full removal.
  148. func (s *ClientService) BulkDetach(inboundSvc *InboundService, emails []string, inboundIds []int) (*BulkDetachResult, bool, error) {
  149. result := &BulkDetachResult{}
  150. if len(emails) == 0 || len(inboundIds) == 0 {
  151. return result, false, nil
  152. }
  153. recordErr := func(format string, args ...any) {
  154. msg := fmt.Sprintf(format, args...)
  155. result.Errors = append(result.Errors, msg)
  156. logger.Warningf("[BulkDetach] %s", msg)
  157. }
  158. requested := make(map[int]struct{}, len(inboundIds))
  159. for _, id := range inboundIds {
  160. requested[id] = struct{}{}
  161. }
  162. recsByInbound := make(map[int][]*model.ClientRecord)
  163. emailOrder := make([]string, 0, len(emails))
  164. emailRepr := make(map[string]string, len(emails))
  165. emailFailed := make(map[string]bool, len(emails))
  166. seenEmail := make(map[string]struct{}, len(emails))
  167. for _, email := range emails {
  168. if email == "" {
  169. continue
  170. }
  171. key := strings.ToLower(email)
  172. if _, ok := seenEmail[key]; ok {
  173. continue
  174. }
  175. seenEmail[key] = struct{}{}
  176. rec, err := s.GetRecordByEmail(nil, email)
  177. if err != nil {
  178. recordErr("%s: %v", email, err)
  179. continue
  180. }
  181. currentIds, err := s.GetInboundIdsForRecord(rec.Id)
  182. if err != nil {
  183. recordErr("%s: %v", email, err)
  184. continue
  185. }
  186. matched := false
  187. for _, id := range currentIds {
  188. if _, ok := requested[id]; ok {
  189. recsByInbound[id] = append(recsByInbound[id], rec)
  190. matched = true
  191. }
  192. }
  193. if !matched {
  194. result.Skipped = append(result.Skipped, rec.Email)
  195. continue
  196. }
  197. emailOrder = append(emailOrder, key)
  198. emailRepr[key] = rec.Email
  199. }
  200. needRestart := false
  201. // Ordered and de-duplicated up front: the sequential loop dropped each map
  202. // entry as it went, which the concurrent applies can no longer do.
  203. detachIds := make([]int, 0, len(recsByInbound))
  204. detachRecs := make([][]*model.ClientRecord, 0, len(recsByInbound))
  205. for _, ibId := range inboundIds {
  206. recs, ok := recsByInbound[ibId]
  207. if !ok {
  208. continue
  209. }
  210. delete(recsByInbound, ibId)
  211. detachIds = append(detachIds, ibId)
  212. detachRecs = append(detachRecs, recs)
  213. }
  214. detachResults, detachPanics := fanoutInboundResults(detachIds, inboundFanoutConcurrency, func(i int) inboundApplyOutcome {
  215. nr, err := s.delInboundClients(inboundSvc, detachIds[i], detachRecs[i], true)
  216. return inboundApplyOutcome{needRestart: nr, err: err}
  217. })
  218. for i, out := range detachResults {
  219. err := out.err
  220. if detachPanics[i] != nil {
  221. // See BulkAttach: a panicking apply may already have committed.
  222. needRestart = true
  223. err = detachPanics[i]
  224. }
  225. if err != nil {
  226. recordErr("inbound %d: %v", detachIds[i], err)
  227. for _, rec := range detachRecs[i] {
  228. emailFailed[strings.ToLower(rec.Email)] = true
  229. }
  230. continue
  231. }
  232. if out.needRestart {
  233. needRestart = true
  234. }
  235. }
  236. for _, key := range emailOrder {
  237. if emailFailed[key] {
  238. continue
  239. }
  240. result.Detached = append(result.Detached, emailRepr[key])
  241. }
  242. return result, needRestart, nil
  243. }
  244. // inboundApplyOutcome carries one inbound's apply result out of the fanout.
  245. type inboundApplyOutcome struct {
  246. needRestart bool
  247. err error
  248. }
  249. // BulkAdjustResult is returned by BulkAdjust to report how many clients were
  250. // successfully updated and which were skipped (typically because the field
  251. // being adjusted was unlimited for that client) or failed.
  252. type BulkAdjustResult struct {
  253. Adjusted int `json:"adjusted"`
  254. Skipped []BulkAdjustReport `json:"skipped,omitempty"`
  255. }
  256. type BulkAdjustReport struct {
  257. Email string `json:"email"`
  258. Reason string `json:"reason"`
  259. }
  260. type bulkAdjustEntry struct {
  261. record *model.ClientRecord
  262. applyExpiry bool
  263. newExpiry int64
  264. applyTotal bool
  265. newTotal int64
  266. }
  267. // bulkFlowClear is the directive that strips the XTLS flow from every selected
  268. // client. The vision values are the only positive flows xray accepts.
  269. const bulkFlowClear = "none"
  270. // bulkFlowAllowed whitelists the flow directives BulkAdjust accepts. Anything
  271. // outside this set is treated as "" (leave flow untouched) so a malformed or
  272. // hostile value can never be injected into a client's settings. The dropdown in
  273. // ClientBulkAdjustModal.tsx offers the same set ("" / "none" / TLS_FLOW_CONTROL);
  274. // keep the two in sync.
  275. var bulkFlowAllowed = map[string]struct{}{
  276. "": {},
  277. bulkFlowClear: {},
  278. "xtls-rprx-vision": {},
  279. "xtls-rprx-vision-udp443": {},
  280. }
  281. // BulkAdjust shifts ExpiryTime by addDays (days) and TotalGB by addBytes
  282. // for every email in the list. Clients whose corresponding field is
  283. // unlimited (0) are skipped — bulk extend should not accidentally
  284. // limit an unlimited client. addDays and addBytes may be negative.
  285. // flow sets the XTLS flow, limitHwid the max registered devices (0 = unlimited)
  286. // and adTag the MTProto sponsor channel; "none" clears flow or adTag.
  287. //
  288. // Like BulkDelete, the work is grouped by inbound so each inbound's
  289. // settings JSON is parsed and written exactly once regardless of how
  290. // many target emails it contains.
  291. func (s *ClientService) BulkAdjust(inboundSvc *InboundService, emails []string, addDays int, addBytes int64, flow string, limitHwid *int, adTag string) (BulkAdjustResult, bool, error) {
  292. result := BulkAdjustResult{}
  293. if len(emails) == 0 {
  294. return result, false, nil
  295. }
  296. flow = strings.TrimSpace(flow)
  297. if _, ok := bulkFlowAllowed[flow]; !ok {
  298. flow = "" // ignore unknown directives — "" means "leave flow untouched"
  299. }
  300. adTag = strings.TrimSpace(adTag)
  301. if adTag != "" && adTag != bulkFlowClear && !model.ValidMtprotoAdTag(adTag) {
  302. return result, false, common.NewError("mtproto client ad tag must be 32 hex characters")
  303. }
  304. if limitHwid != nil && *limitHwid < 0 {
  305. zero := 0
  306. limitHwid = &zero
  307. }
  308. adjustFlow := flow != ""
  309. adjustHwid := limitHwid != nil
  310. adjustAdTag := adTag != ""
  311. if addDays == 0 && addBytes == 0 && !adjustFlow && !adjustHwid && !adjustAdTag {
  312. return result, false, common.NewError("no adjustment specified")
  313. }
  314. addExpiryMs := int64(addDays) * 24 * 60 * 60 * 1000
  315. seen := map[string]struct{}{}
  316. cleanEmails := make([]string, 0, len(emails))
  317. for _, e := range emails {
  318. e = strings.TrimSpace(e)
  319. if e == "" {
  320. continue
  321. }
  322. if _, ok := seen[e]; ok {
  323. continue
  324. }
  325. seen[e] = struct{}{}
  326. cleanEmails = append(cleanEmails, e)
  327. }
  328. if len(cleanEmails) == 0 {
  329. return result, false, nil
  330. }
  331. db := database.GetDB()
  332. var records []model.ClientRecord
  333. for _, batch := range chunkStrings(cleanEmails, sqlInChunk) {
  334. var rows []model.ClientRecord
  335. if err := db.Where("email IN ?", batch).Find(&rows).Error; err != nil {
  336. return result, false, err
  337. }
  338. records = append(records, rows...)
  339. }
  340. recordsByEmail := make(map[string]*model.ClientRecord, len(records))
  341. for i := range records {
  342. recordsByEmail[records[i].Email] = &records[i]
  343. }
  344. skippedReasons := map[string]string{}
  345. for _, email := range cleanEmails {
  346. if _, ok := recordsByEmail[email]; !ok {
  347. skippedReasons[email] = "client not found"
  348. }
  349. }
  350. plan := map[string]*bulkAdjustEntry{}
  351. for email, rec := range recordsByEmail {
  352. entry := &bulkAdjustEntry{record: rec}
  353. if addDays != 0 {
  354. switch {
  355. case rec.ExpiryTime == 0:
  356. if _, exists := skippedReasons[email]; !exists {
  357. skippedReasons[email] = "unlimited expiry"
  358. }
  359. case rec.ExpiryTime > 0:
  360. next := rec.ExpiryTime + addExpiryMs
  361. if next <= 0 {
  362. if _, exists := skippedReasons[email]; !exists {
  363. skippedReasons[email] = "reduction exceeds remaining time"
  364. }
  365. } else {
  366. entry.applyExpiry = true
  367. entry.newExpiry = next
  368. }
  369. default:
  370. next := rec.ExpiryTime - addExpiryMs
  371. if next >= 0 {
  372. if _, exists := skippedReasons[email]; !exists {
  373. skippedReasons[email] = "reduction exceeds delay window"
  374. }
  375. } else {
  376. entry.applyExpiry = true
  377. entry.newExpiry = next
  378. }
  379. }
  380. }
  381. if addBytes != 0 {
  382. if rec.TotalGB == 0 {
  383. if _, exists := skippedReasons[email]; !exists {
  384. skippedReasons[email] = "unlimited traffic"
  385. }
  386. } else {
  387. next := rec.TotalGB + addBytes
  388. if next <= 0 {
  389. if _, exists := skippedReasons[email]; !exists {
  390. skippedReasons[email] = "reduction exceeds remaining quota"
  391. }
  392. } else {
  393. entry.applyTotal = true
  394. entry.newTotal = next
  395. }
  396. }
  397. }
  398. if entry.applyExpiry || entry.applyTotal || adjustFlow || adjustHwid || adjustAdTag {
  399. plan[email] = entry
  400. }
  401. }
  402. if len(plan) == 0 {
  403. for email, reason := range skippedReasons {
  404. result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: reason})
  405. }
  406. return result, false, nil
  407. }
  408. plannedIds := make([]int, 0, len(plan))
  409. recordIdToEmail := make(map[int]string, len(plan))
  410. for email, entry := range plan {
  411. if entry.applyExpiry || entry.applyTotal || adjustFlow || adjustAdTag {
  412. plannedIds = append(plannedIds, entry.record.Id)
  413. recordIdToEmail[entry.record.Id] = email
  414. }
  415. }
  416. var mappings []model.ClientInbound
  417. for _, batch := range chunkInts(plannedIds, sqlInChunk) {
  418. var rows []model.ClientInbound
  419. if err := db.Where("client_id IN ?", batch).Find(&rows).Error; err != nil {
  420. return result, false, err
  421. }
  422. mappings = append(mappings, rows...)
  423. }
  424. emailsByInbound := map[int][]string{}
  425. for _, m := range mappings {
  426. email, ok := recordIdToEmail[m.ClientId]
  427. if !ok {
  428. continue
  429. }
  430. emailsByInbound[m.InboundId] = append(emailsByInbound[m.InboundId], email)
  431. }
  432. needRestart := false
  433. flowHonored := map[string]bool{}
  434. flowIneligible := map[string]bool{}
  435. adTagHonored := map[string]bool{}
  436. adTagIneligible := map[string]bool{}
  437. execFailed := map[string]bool{}
  438. adjustIds := sortedInboundIds(emailsByInbound)
  439. adjustResults, adjustPanics := fanoutInboundResults(adjustIds, inboundFanoutConcurrency, func(i int) bulkInboundAdjustResult {
  440. return s.bulkAdjustInboundClients(inboundSvc, adjustIds[i], emailsByInbound[adjustIds[i]], plan, flow, adTag)
  441. })
  442. for i, ibRes := range adjustResults {
  443. if adjustPanics[i] != nil {
  444. needRestart = true
  445. for _, email := range emailsByInbound[adjustIds[i]] {
  446. execFailed[email] = true
  447. if _, already := skippedReasons[email]; !already {
  448. skippedReasons[email] = adjustPanics[i].Error()
  449. }
  450. }
  451. continue
  452. }
  453. if ibRes.needRestart {
  454. needRestart = true
  455. }
  456. for email := range ibRes.flowHonored {
  457. flowHonored[email] = true
  458. }
  459. for email := range ibRes.flowIneligible {
  460. flowIneligible[email] = true
  461. }
  462. for email := range ibRes.adTagHonored {
  463. adTagHonored[email] = true
  464. }
  465. for email := range ibRes.adTagIneligible {
  466. adTagIneligible[email] = true
  467. }
  468. for email, reason := range ibRes.perEmailSkipped {
  469. execFailed[email] = true
  470. if _, already := skippedReasons[email]; !already {
  471. skippedReasons[email] = reason
  472. }
  473. }
  474. }
  475. cond, condArgs := depletedCond(db)
  476. candidateEmails := make([]string, 0, len(plan))
  477. for email, entry := range plan {
  478. if entry.applyExpiry || entry.applyTotal {
  479. candidateEmails = append(candidateEmails, email)
  480. }
  481. }
  482. wasDisabledDepleted := map[string]struct{}{}
  483. for _, batch := range chunkStrings(candidateEmails, sqlInChunk) {
  484. var rows []string
  485. if err := db.Model(xray.ClientTraffic{}).
  486. Where(cond+" AND enable = ? AND email IN ?", append(append([]any{}, condArgs...), false, batch)...).
  487. Pluck("email", &rows).Error; err != nil {
  488. return result, needRestart, err
  489. }
  490. for _, e := range rows {
  491. wasDisabledDepleted[e] = struct{}{}
  492. }
  493. }
  494. wantAdTag := ""
  495. if adjustAdTag && adTag != bulkFlowClear {
  496. wantAdTag = strings.ToLower(adTag)
  497. }
  498. adjusted := map[string]struct{}{}
  499. for email, entry := range plan {
  500. if execFailed[email] {
  501. continue
  502. }
  503. updates := map[string]any{}
  504. if entry.applyExpiry {
  505. updates["expiry_time"] = entry.newExpiry
  506. }
  507. if entry.applyTotal {
  508. updates["total"] = entry.newTotal
  509. }
  510. if len(updates) > 0 {
  511. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).Updates(updates).Error; err != nil {
  512. if _, already := skippedReasons[email]; !already {
  513. skippedReasons[email] = err.Error()
  514. }
  515. continue
  516. }
  517. }
  518. if adjustHwid {
  519. if err := s.setClientLimitHwidByEmail(db, email, *limitHwid); err != nil {
  520. if _, already := skippedReasons[email]; !already {
  521. skippedReasons[email] = err.Error()
  522. }
  523. continue
  524. }
  525. }
  526. if adjustAdTag && adTagHonored[email] {
  527. if err := db.Model(&model.ClientRecord{}).Where("email = ?", email).UpdateColumn("ad_tag", wantAdTag).Error; err != nil {
  528. if _, already := skippedReasons[email]; !already {
  529. skippedReasons[email] = err.Error()
  530. }
  531. continue
  532. }
  533. }
  534. // Counted when expiry/total changed, flow was honored, adTag was honored, or limitHwid was adjusted.
  535. if len(updates) > 0 || flowHonored[email] || adTagHonored[email] || adjustHwid {
  536. adjusted[email] = struct{}{}
  537. }
  538. }
  539. result.Adjusted = len(adjusted)
  540. for email, reason := range skippedReasons {
  541. result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: reason})
  542. }
  543. // Report a flow directive that no inbound could carry — only when it was not
  544. // honored anywhere and the client has no other (expiry/total) skip reason.
  545. // The expiry/total part, if any, has already been applied and counted above.
  546. for email := range flowIneligible {
  547. if flowHonored[email] {
  548. continue
  549. }
  550. if _, already := skippedReasons[email]; already {
  551. continue
  552. }
  553. result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: "flow not supported on inbound"})
  554. }
  555. for email := range adTagIneligible {
  556. if adTagHonored[email] {
  557. continue
  558. }
  559. if _, already := skippedReasons[email]; already {
  560. continue
  561. }
  562. result.Skipped = append(result.Skipped, BulkAdjustReport{Email: email, Reason: "adTag not supported on inbound"})
  563. }
  564. if len(wasDisabledDepleted) > 0 {
  565. stillDepleted := map[string]struct{}{}
  566. wasList := make([]string, 0, len(wasDisabledDepleted))
  567. for e := range wasDisabledDepleted {
  568. wasList = append(wasList, e)
  569. }
  570. for _, batch := range chunkStrings(wasList, sqlInChunk) {
  571. var rows []string
  572. if err := db.Model(xray.ClientTraffic{}).
  573. Where(cond+" AND email IN ?", append(append([]any{}, condArgs...), batch)...).
  574. Pluck("email", &rows).Error; err != nil {
  575. return result, needRestart, err
  576. }
  577. for _, e := range rows {
  578. stillDepleted[e] = struct{}{}
  579. }
  580. }
  581. reEnable := make([]string, 0, len(wasDisabledDepleted))
  582. for e := range wasDisabledDepleted {
  583. if _, still := stillDepleted[e]; !still {
  584. reEnable = append(reEnable, e)
  585. }
  586. }
  587. if len(reEnable) > 0 {
  588. _, nr, err := s.BulkSetEnable(inboundSvc, reEnable, true)
  589. if err != nil {
  590. return result, needRestart, err
  591. }
  592. if nr {
  593. needRestart = true
  594. }
  595. }
  596. }
  597. return result, needRestart, nil
  598. }
  599. type bulkInboundAdjustResult struct {
  600. perEmailSkipped map[string]string
  601. flowHonored map[string]bool
  602. // flowIneligible is tracked apart from perEmailSkipped: a flow directive
  603. // that an inbound cannot carry must not suppress the expiry/total write for
  604. // the same client (which would diverge the inbound JSON / ClientRecord from
  605. // ClientTraffic). It only feeds the final Skipped report.
  606. flowIneligible map[string]bool
  607. adTagHonored map[string]bool
  608. adTagIneligible map[string]bool
  609. needRestart bool
  610. }
  611. // bulkAdjustInboundClients applies expiry/total deltas to multiple clients
  612. // inside a single inbound's settings JSON. The xray runtime is updated
  613. // only for remote-node inbounds; local nodes do not need a notification
  614. // because the AddUser payload does not include totalGB/expiryTime —
  615. // changing those fields is identity-preserving and the panel's traffic
  616. // enforcement loop picks up the new limits from ClientTraffic directly.
  617. func (s *ClientService) bulkAdjustInboundClients(
  618. inboundSvc *InboundService,
  619. inboundId int,
  620. emails []string,
  621. plan map[string]*bulkAdjustEntry,
  622. flow string,
  623. adTag string,
  624. ) bulkInboundAdjustResult {
  625. res := bulkInboundAdjustResult{
  626. perEmailSkipped: map[string]string{},
  627. flowHonored: map[string]bool{},
  628. flowIneligible: map[string]bool{},
  629. adTagHonored: map[string]bool{},
  630. adTagIneligible: map[string]bool{},
  631. }
  632. defer lockInbound(inboundId).Unlock()
  633. oldInbound, err := inboundSvc.GetInbound(inboundId)
  634. if err != nil {
  635. logger.Error("Load Old Data Error")
  636. for _, e := range emails {
  637. res.perEmailSkipped[e] = err.Error()
  638. }
  639. return res
  640. }
  641. var settings map[string]any
  642. if err := json.Unmarshal([]byte(oldInbound.Settings), &settings); err != nil {
  643. for _, e := range emails {
  644. res.perEmailSkipped[e] = err.Error()
  645. }
  646. return res
  647. }
  648. // Match by email — the client's stable identity (see Delete). Credentials
  649. // can drift from the inbound JSON, so they are never used for matching.
  650. wantedEmails := make(map[string]struct{}, len(emails))
  651. for _, email := range emails {
  652. if plan[email] == nil {
  653. res.perEmailSkipped[email] = "client not found"
  654. continue
  655. }
  656. wantedEmails[email] = struct{}{}
  657. }
  658. // Flow eligibility is a property of the inbound (protocol + transport), so
  659. // resolve it once. Clearing flow is always allowed; setting a vision flow
  660. // is only honored on an inbound that can carry it.
  661. flowEligible := flow == bulkFlowClear ||
  662. (!oldInbound.DisableFlow &&
  663. inboundCanEnableTlsFlow(string(oldInbound.Protocol), oldInbound.StreamSettings, oldInbound.Settings))
  664. wantAdTag := ""
  665. if adTag != "" && adTag != bulkFlowClear {
  666. wantAdTag = strings.ToLower(adTag)
  667. }
  668. interfaceClients, _ := settings["clients"].([]any)
  669. foundEmails := map[string]bool{}
  670. flowChanged := false
  671. adTagChanged := false
  672. hasInboundChanges := false
  673. nowMs := time.Now().Unix() * 1000
  674. for i, client := range interfaceClients {
  675. c, ok := client.(map[string]any)
  676. if !ok {
  677. continue
  678. }
  679. targetEmail, _ := c["email"].(string)
  680. if _, want := wantedEmails[targetEmail]; !want || targetEmail == "" {
  681. continue
  682. }
  683. clientChanged := false
  684. entry := plan[targetEmail]
  685. if entry.applyExpiry {
  686. c["expiryTime"] = entry.newExpiry
  687. clientChanged = true
  688. }
  689. if entry.applyTotal {
  690. c["totalGB"] = entry.newTotal
  691. clientChanged = true
  692. }
  693. if flow != "" {
  694. if flowEligible {
  695. want := ""
  696. if flow != bulkFlowClear {
  697. want = flow
  698. }
  699. if cur, _ := c["flow"].(string); cur != want {
  700. c["flow"] = want
  701. flowChanged = true
  702. }
  703. res.flowHonored[targetEmail] = true
  704. clientChanged = true
  705. } else {
  706. // Record separately so this never suppresses the expiry/total
  707. // write for the same client (see flowIneligible doc).
  708. res.flowIneligible[targetEmail] = true
  709. }
  710. }
  711. if adTag != "" {
  712. if oldInbound.Protocol == model.MTProto {
  713. if cur, _ := c["adTag"].(string); cur != wantAdTag {
  714. c["adTag"] = wantAdTag
  715. adTagChanged = true
  716. }
  717. res.adTagHonored[targetEmail] = true
  718. clientChanged = true
  719. } else {
  720. res.adTagIneligible[targetEmail] = true
  721. }
  722. }
  723. if clientChanged {
  724. c["updated_at"] = nowMs
  725. hasInboundChanges = true
  726. }
  727. interfaceClients[i] = c
  728. foundEmails[targetEmail] = true
  729. }
  730. for email := range wantedEmails {
  731. if !foundEmails[email] {
  732. res.perEmailSkipped[email] = "Client Not Found In Inbound"
  733. }
  734. }
  735. if len(foundEmails) == 0 || !hasInboundChanges {
  736. return res
  737. }
  738. settings["clients"] = interfaceClients
  739. newSettings, err := json.MarshalIndent(settings, "", " ")
  740. if err != nil {
  741. for email := range foundEmails {
  742. res.perEmailSkipped[email] = err.Error()
  743. }
  744. return res
  745. }
  746. oldInbound.Settings = string(newSettings)
  747. // A flow change rewrites the user's xray config, which the lightweight
  748. // UpdateUser push below does not carry. Local nodes reload via restart;
  749. // remote nodes get a full reconcile (MarkNodeDirty) instead of a per-user push.
  750. if flowChanged && oldInbound.NodeID == nil {
  751. res.needRestart = true
  752. }
  753. // Serialize against the traffic poll to avoid the cross-transaction
  754. // lock-order deadlock on inbounds/client_records (runSerializedTx).
  755. txErr := runSerializedTx(func(tx *gorm.DB) error {
  756. if err := tx.Save(oldInbound).Error; err != nil {
  757. return err
  758. }
  759. finalClients, gcErr := inboundSvc.GetClients(oldInbound)
  760. if gcErr != nil {
  761. return gcErr
  762. }
  763. if err := s.SyncInbound(tx, inboundId, finalClients); err != nil {
  764. return err
  765. }
  766. if oldInbound.NodeID != nil {
  767. return (&NodeService{}).MarkNodeDirtyTx(tx, *oldInbound.NodeID)
  768. }
  769. return nil
  770. })
  771. if txErr != nil {
  772. for email := range foundEmails {
  773. if _, skip := res.perEmailSkipped[email]; !skip {
  774. res.perEmailSkipped[email] = txErr.Error()
  775. }
  776. }
  777. } else {
  778. if adTagChanged && oldInbound.Protocol == model.MTProto && oldInbound.NodeID == nil {
  779. inboundSvc.applyLocalMtproto(oldInbound.Id)
  780. }
  781. if oldInbound.NodeID != nil && !flowChanged && len(foundEmails) <= nodeBulkPushThreshold {
  782. rt, push, _, perr := inboundSvc.nodePushPlan(oldInbound)
  783. if perr != nil {
  784. logger.Warning("BulkAdjust: node runtime lookup after commit failed:", perr)
  785. } else if push {
  786. for email := range foundEmails {
  787. entry := plan[email]
  788. updated := *entry.record.ToClient()
  789. if entry.applyExpiry {
  790. updated.ExpiryTime = entry.newExpiry
  791. }
  792. if entry.applyTotal {
  793. updated.TotalGB = entry.newTotal
  794. }
  795. if adTag != "" && oldInbound.Protocol == model.MTProto {
  796. updated.AdTag = wantAdTag
  797. }
  798. updated.UpdatedAt = nowMs
  799. ctx, cancel := nodePushContext()
  800. err1 := rt.UpdateUser(ctx, oldInbound, email, updated)
  801. cancel()
  802. if err1 != nil {
  803. logger.Warning("Error in updating client on", rt.Name(), ":", err1)
  804. // First failure ends the batch push; the reconcile converges the rest.
  805. break
  806. }
  807. }
  808. }
  809. }
  810. }
  811. return res
  812. }
  813. // BulkDeleteResult mirrors BulkAdjustResult: total deleted plus per-email
  814. // skip reasons when an email could not be processed.
  815. type BulkDeleteResult struct {
  816. Deleted int `json:"deleted"`
  817. Skipped []BulkDeleteReport `json:"skipped,omitempty"`
  818. }
  819. type BulkDeleteReport struct {
  820. Email string `json:"email"`
  821. Reason string `json:"reason"`
  822. }
  823. // BulkDelete removes every client in the list in one optimized pass.
  824. // Instead of running the full single-delete pipeline N times (which would
  825. // re-read, re-parse, and re-write each inbound's settings JSON for every
  826. // email), it groups emails by inbound and performs a single
  827. // read-modify-write per inbound. Per-row DB cleanups are also batched with
  828. // IN-clause queries at the end. Errors on a particular email are recorded
  829. // in the Skipped list and processing continues for the rest.
  830. func (s *ClientService) BulkDelete(inboundSvc *InboundService, emails []string, keepTraffic bool) (BulkDeleteResult, bool, error) {
  831. result := BulkDeleteResult{}
  832. seen := map[string]struct{}{}
  833. cleanEmails := make([]string, 0, len(emails))
  834. for _, e := range emails {
  835. e = strings.TrimSpace(e)
  836. if e == "" {
  837. continue
  838. }
  839. if _, ok := seen[e]; ok {
  840. continue
  841. }
  842. seen[e] = struct{}{}
  843. cleanEmails = append(cleanEmails, e)
  844. }
  845. if len(cleanEmails) == 0 {
  846. return result, false, nil
  847. }
  848. db := database.GetDB()
  849. var records []model.ClientRecord
  850. for _, batch := range chunkStrings(cleanEmails, sqlInChunk) {
  851. var rows []model.ClientRecord
  852. if err := db.Where("email IN ?", batch).Find(&rows).Error; err != nil {
  853. return result, false, err
  854. }
  855. records = append(records, rows...)
  856. }
  857. recordsByEmail := make(map[string]*model.ClientRecord, len(records))
  858. tombstoneEmails := make([]string, 0, len(records))
  859. for i := range records {
  860. recordsByEmail[records[i].Email] = &records[i]
  861. tombstoneEmails = append(tombstoneEmails, records[i].Email)
  862. }
  863. tombstoneClientEmails(tombstoneEmails)
  864. skippedReasons := map[string]string{}
  865. for _, email := range cleanEmails {
  866. if _, ok := recordsByEmail[email]; !ok {
  867. skippedReasons[email] = "client not found"
  868. }
  869. }
  870. clientIds := make([]int, 0, len(recordsByEmail))
  871. recordIdToEmail := make(map[int]string, len(recordsByEmail))
  872. for _, r := range recordsByEmail {
  873. clientIds = append(clientIds, r.Id)
  874. recordIdToEmail[r.Id] = r.Email
  875. }
  876. emailsByInbound := map[int][]string{}
  877. if len(clientIds) > 0 {
  878. var mappings []model.ClientInbound
  879. for _, batch := range chunkInts(clientIds, sqlInChunk) {
  880. var rows []model.ClientInbound
  881. if err := db.Where("client_id IN ?", batch).Find(&rows).Error; err != nil {
  882. return result, false, err
  883. }
  884. mappings = append(mappings, rows...)
  885. }
  886. for _, m := range mappings {
  887. email, ok := recordIdToEmail[m.ClientId]
  888. if !ok {
  889. continue
  890. }
  891. emailsByInbound[m.InboundId] = append(emailsByInbound[m.InboundId], email)
  892. }
  893. }
  894. needRestart := false
  895. delIds := sortedInboundIds(emailsByInbound)
  896. delResults, delPanics := fanoutInboundResults(delIds, inboundFanoutConcurrency, func(i int) bulkInboundDeleteResult {
  897. return s.bulkDelInboundClients(inboundSvc, delIds[i], emailsByInbound[delIds[i]], recordsByEmail, keepTraffic)
  898. })
  899. for i, ibResult := range delResults {
  900. if delPanics[i] != nil {
  901. needRestart = true
  902. for _, email := range emailsByInbound[delIds[i]] {
  903. if _, already := skippedReasons[email]; !already {
  904. skippedReasons[email] = delPanics[i].Error()
  905. }
  906. }
  907. continue
  908. }
  909. if ibResult.needRestart {
  910. needRestart = true
  911. }
  912. for email, reason := range ibResult.perEmailSkipped {
  913. if _, already := skippedReasons[email]; !already {
  914. skippedReasons[email] = reason
  915. }
  916. }
  917. }
  918. successEmails := make([]string, 0, len(recordsByEmail))
  919. successIds := make([]int, 0, len(recordsByEmail))
  920. failedEmails := make([]string, 0, len(recordsByEmail))
  921. successSubIDs := make([]string, 0, len(recordsByEmail))
  922. for email, rec := range recordsByEmail {
  923. if _, skipped := skippedReasons[email]; skipped {
  924. failedEmails = append(failedEmails, email)
  925. continue
  926. }
  927. successEmails = append(successEmails, email)
  928. successIds = append(successIds, rec.Id)
  929. successSubIDs = append(successSubIDs, rec.SubID)
  930. }
  931. withdrawClientTombstones(failedEmails...)
  932. if len(successIds) > 0 {
  933. // Serialize the row cleanup against the traffic poll to avoid the
  934. // cross-transaction lock-order deadlock on client_traffics/inbounds.
  935. if err := runSerializedTx(func(tx *gorm.DB) error {
  936. if e := adjustGroupBaselinesForRemovedTraffic(tx, successEmails); e != nil {
  937. return e
  938. }
  939. if e := clearClientHwidsBySubIDTx(tx, successSubIDs...); e != nil {
  940. return e
  941. }
  942. for _, batch := range chunkInts(successIds, sqlInChunk) {
  943. if e := tx.Where("client_id IN ?", batch).Delete(&model.ClientInbound{}).Error; e != nil {
  944. return e
  945. }
  946. if e := tx.Where("client_id IN ?", batch).Delete(&model.ClientExternalLink{}).Error; e != nil {
  947. return e
  948. }
  949. }
  950. if !keepTraffic && len(successEmails) > 0 {
  951. for _, batch := range chunkStrings(successEmails, sqlInChunk) {
  952. if e := tx.Where("email IN ?", batch).Delete(&xray.ClientTraffic{}).Error; e != nil {
  953. return e
  954. }
  955. if e := tx.Where("client_email IN ?", batch).Delete(&model.InboundClientIps{}).Error; e != nil {
  956. return e
  957. }
  958. }
  959. }
  960. for _, batch := range chunkInts(successIds, sqlInChunk) {
  961. if e := tx.Where("id IN ?", batch).Delete(&model.ClientRecord{}).Error; e != nil {
  962. return e
  963. }
  964. }
  965. return nil
  966. }); err != nil {
  967. withdrawClientTombstones(successEmails...)
  968. return result, needRestart, err
  969. }
  970. }
  971. result.Deleted = len(successEmails)
  972. for email, reason := range skippedReasons {
  973. result.Skipped = append(result.Skipped, BulkDeleteReport{Email: email, Reason: reason})
  974. }
  975. return result, needRestart, nil
  976. }
  977. type bulkInboundDeleteResult struct {
  978. perEmailSkipped map[string]string
  979. needRestart bool
  980. }
  981. // bulkDelInboundClients removes multiple clients from a single inbound's
  982. // settings JSON in one read-modify-write cycle, runs the xray runtime
  983. // RemoveUser/DeleteUser calls, and persists the inbound. The returned map
  984. // holds per-email failure reasons; emails not present in the map are
  985. // considered successful for this inbound.
  986. func (s *ClientService) bulkDelInboundClients(
  987. inboundSvc *InboundService,
  988. inboundId int,
  989. emails []string,
  990. records map[string]*model.ClientRecord,
  991. keepTraffic bool,
  992. ) bulkInboundDeleteResult {
  993. res := bulkInboundDeleteResult{perEmailSkipped: map[string]string{}}
  994. defer lockInbound(inboundId).Unlock()
  995. oldInbound, err := inboundSvc.GetInbound(inboundId)
  996. if err != nil {
  997. logger.Error("Load Old Data Error")
  998. for _, e := range emails {
  999. res.perEmailSkipped[e] = err.Error()
  1000. }
  1001. return res
  1002. }
  1003. var settings map[string]any
  1004. if err := json.Unmarshal([]byte(oldInbound.Settings), &settings); err != nil {
  1005. for _, e := range emails {
  1006. res.perEmailSkipped[e] = err.Error()
  1007. }
  1008. return res
  1009. }
  1010. // Match by email — the client's stable identity (see Delete). The link-derived
  1011. // set is deletion intent: an email already absent from settings is successful,
  1012. // while foundEmails tracks entries that still need settings-specific cleanup.
  1013. wantedEmails := make(map[string]struct{}, len(emails))
  1014. for _, email := range emails {
  1015. if records[email] == nil {
  1016. res.perEmailSkipped[email] = "client not found"
  1017. continue
  1018. }
  1019. wantedEmails[email] = struct{}{}
  1020. }
  1021. interfaceClients, _ := settings["clients"].([]any)
  1022. newClients := make([]any, 0, len(interfaceClients))
  1023. foundEmails := map[string]bool{}
  1024. enableByEmail := map[string]bool{}
  1025. for _, client := range interfaceClients {
  1026. c, ok := client.(map[string]any)
  1027. if !ok {
  1028. newClients = append(newClients, client)
  1029. continue
  1030. }
  1031. em, _ := c["email"].(string)
  1032. if _, found := wantedEmails[em]; found && em != "" {
  1033. foundEmails[em] = true
  1034. en, _ := c["enable"].(bool)
  1035. enableByEmail[em] = en
  1036. continue
  1037. }
  1038. newClients = append(newClients, client)
  1039. }
  1040. db := database.GetDB()
  1041. newClients = compactOrphans(db, newClients)
  1042. if newClients == nil {
  1043. newClients = []any{}
  1044. }
  1045. settings["clients"] = newClients
  1046. newSettings, err := json.MarshalIndent(settings, "", " ")
  1047. if err != nil {
  1048. for email := range wantedEmails {
  1049. if _, skip := res.perEmailSkipped[email]; !skip {
  1050. res.perEmailSkipped[email] = err.Error()
  1051. }
  1052. }
  1053. return res
  1054. }
  1055. oldInbound.Settings = string(newSettings)
  1056. foundList := make([]string, 0, len(foundEmails))
  1057. for email := range foundEmails {
  1058. foundList = append(foundList, email)
  1059. }
  1060. notDepletedByEmail := map[string]bool{}
  1061. if len(foundList) > 0 {
  1062. type trafficRow struct {
  1063. Email string
  1064. Enable bool
  1065. }
  1066. for _, batch := range chunkStrings(foundList, sqlInChunk) {
  1067. var rows []trafficRow
  1068. if err := db.Model(xray.ClientTraffic{}).
  1069. Where("email IN ?", batch).
  1070. Select("email, enable").
  1071. Scan(&rows).Error; err == nil {
  1072. for _, r := range rows {
  1073. notDepletedByEmail[r.Email] = r.Enable
  1074. }
  1075. }
  1076. }
  1077. }
  1078. var sharedSet map[string]bool
  1079. if !keepTraffic {
  1080. var sharedErr error
  1081. sharedSet, sharedErr = inboundSvc.emailsUsedByOtherInbounds(foundList, inboundId)
  1082. if sharedErr != nil {
  1083. for email := range wantedEmails {
  1084. res.perEmailSkipped[email] = sharedErr.Error()
  1085. }
  1086. return res
  1087. }
  1088. }
  1089. if !keepTraffic {
  1090. purge := make([]string, 0, len(foundEmails))
  1091. for email := range foundEmails {
  1092. if !sharedSet[strings.ToLower(strings.TrimSpace(email))] {
  1093. purge = append(purge, email)
  1094. }
  1095. }
  1096. if len(purge) > 0 {
  1097. // Serialize the IP/stat purge against the traffic poll to avoid the
  1098. // cross-transaction lock-order deadlock on client_traffics.
  1099. if delErr := runSerializedTx(func(tx *gorm.DB) error {
  1100. if e := inboundSvc.delClientIPsByEmails(tx, purge); e != nil {
  1101. logger.Error("Error in delete client IPs")
  1102. return e
  1103. }
  1104. if e := inboundSvc.delClientStatsByEmails(tx, purge); e != nil {
  1105. logger.Error("Delete stats Data Error")
  1106. return e
  1107. }
  1108. return nil
  1109. }); delErr != nil {
  1110. for _, email := range purge {
  1111. res.perEmailSkipped[email] = delErr.Error()
  1112. delete(foundEmails, email)
  1113. }
  1114. }
  1115. }
  1116. }
  1117. // Serialize against the traffic poll to avoid the cross-transaction
  1118. // lock-order deadlock on inbounds/client_records (runSerializedTx).
  1119. txErr := runSerializedTx(func(tx *gorm.DB) error {
  1120. if err := tx.Save(oldInbound).Error; err != nil {
  1121. return err
  1122. }
  1123. finalClients, err := inboundSvc.GetClients(oldInbound)
  1124. if err != nil {
  1125. return err
  1126. }
  1127. if err := s.SyncInbound(tx, inboundId, finalClients); err != nil {
  1128. return err
  1129. }
  1130. if oldInbound.NodeID != nil {
  1131. return (&NodeService{}).MarkNodeDirtyTx(tx, *oldInbound.NodeID)
  1132. }
  1133. return nil
  1134. })
  1135. if txErr != nil {
  1136. for email := range wantedEmails {
  1137. if _, skip := res.perEmailSkipped[email]; !skip {
  1138. res.perEmailSkipped[email] = txErr.Error()
  1139. }
  1140. }
  1141. } else if oldInbound.NodeID == nil {
  1142. rt, rterr := inboundSvc.runtimeFor(oldInbound)
  1143. if rterr != nil {
  1144. res.needRestart = true
  1145. } else {
  1146. for email := range foundEmails {
  1147. if !enableByEmail[email] || !notDepletedByEmail[email] {
  1148. continue
  1149. }
  1150. err1 := rt.RemoveUser(context.Background(), oldInbound, email)
  1151. if err1 == nil {
  1152. logger.Debug("Client deleted on", rt.Name(), ":", email)
  1153. } else if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", email)) {
  1154. logger.Debug("User is already deleted. Nothing to do more...")
  1155. } else {
  1156. logger.Debug("Error in deleting client on", rt.Name(), ":", err1)
  1157. res.needRestart = true
  1158. }
  1159. }
  1160. }
  1161. } else {
  1162. dispatchEmails := make([]string, 0, len(wantedEmails))
  1163. for email := range wantedEmails {
  1164. if _, skip := res.perEmailSkipped[email]; !skip {
  1165. dispatchEmails = append(dispatchEmails, email)
  1166. }
  1167. }
  1168. if len(dispatchEmails) > nodeBulkPushThreshold {
  1169. return res
  1170. }
  1171. rt, push, _, perr := inboundSvc.nodePushPlan(oldInbound)
  1172. if perr != nil {
  1173. logger.Warning("BulkDelete: node runtime lookup after commit failed:", perr)
  1174. } else if push {
  1175. for _, email := range dispatchEmails {
  1176. ctx, cancel := nodePushContext()
  1177. err1 := rt.DeleteClient(ctx, email)
  1178. cancel()
  1179. if err1 != nil {
  1180. logger.Warning("Error in deleting client on", rt.Name(), ":", err1)
  1181. // The node is already dirty, so one reconcile converges the rest of
  1182. // the batch instead of paying another deadline per client.
  1183. break
  1184. }
  1185. }
  1186. }
  1187. }
  1188. return res
  1189. }
  1190. // BulkCreateResult mirrors BulkAdjustResult for the create flow.
  1191. type BulkCreateResult struct {
  1192. Created int `json:"created"`
  1193. Skipped []BulkCreateReport `json:"skipped,omitempty"`
  1194. }
  1195. type BulkCreateReport struct {
  1196. Email string `json:"email"`
  1197. Reason string `json:"reason"`
  1198. }
  1199. func (s *ClientService) BulkCreate(inboundSvc *InboundService, payloads []ClientCreatePayload) (BulkCreateResult, bool, error) {
  1200. result := BulkCreateResult{}
  1201. if len(payloads) == 0 {
  1202. return result, false, nil
  1203. }
  1204. skip := func(email, reason string) {
  1205. if strings.TrimSpace(email) == "" {
  1206. email = "(missing email)"
  1207. }
  1208. result.Skipped = append(result.Skipped, BulkCreateReport{Email: email, Reason: reason})
  1209. }
  1210. type prepared struct {
  1211. client model.Client
  1212. inboundIds []int
  1213. limitHwid int
  1214. }
  1215. prep := make([]prepared, 0, len(payloads))
  1216. emails := make([]string, 0, len(payloads))
  1217. subIDs := make([]string, 0, len(payloads))
  1218. seenEmail := make(map[string]struct{}, len(payloads))
  1219. seenSubID := make(map[string]string, len(payloads))
  1220. for i := range payloads {
  1221. client := payloads[i].Client
  1222. email := strings.TrimSpace(client.Email)
  1223. if email == "" {
  1224. skip("", "client email is required")
  1225. continue
  1226. }
  1227. if verr := validateClientEmail(email); verr != nil {
  1228. skip(email, verr.Error())
  1229. continue
  1230. }
  1231. if verr := validateClientSubID(client.SubID); verr != nil {
  1232. skip(email, verr.Error())
  1233. continue
  1234. }
  1235. if verr := validateClientResetDay(client.ResetDay); verr != nil {
  1236. skip(email, verr.Error())
  1237. continue
  1238. }
  1239. if verr := validateClientResetMax(client.ResetMax); verr != nil {
  1240. skip(email, verr.Error())
  1241. continue
  1242. }
  1243. if verr := validateClientTrafficReset(client.TrafficReset, client.TrafficResetDay); verr != nil {
  1244. skip(email, verr.Error())
  1245. continue
  1246. }
  1247. if len(payloads[i].InboundIds) == 0 {
  1248. skip(email, "at least one inbound is required")
  1249. continue
  1250. }
  1251. client.Email = email
  1252. if client.SubID == "" {
  1253. client.SubID = uuid.NewString()
  1254. }
  1255. if !client.Enable {
  1256. client.Enable = true
  1257. }
  1258. now := time.Now().UnixMilli()
  1259. if client.CreatedAt == 0 {
  1260. client.CreatedAt = now
  1261. }
  1262. client.UpdatedAt = now
  1263. le := strings.ToLower(email)
  1264. if _, dup := seenEmail[le]; dup {
  1265. skip(email, "email already in use: "+email)
  1266. continue
  1267. }
  1268. if owner, ok := seenSubID[client.SubID]; ok && owner != le {
  1269. skip(email, "subId already in use: "+client.SubID)
  1270. continue
  1271. }
  1272. seenEmail[le] = struct{}{}
  1273. seenSubID[client.SubID] = le
  1274. prep = append(prep, prepared{client: client, inboundIds: payloads[i].InboundIds, limitHwid: payloads[i].LimitHwid})
  1275. emails = append(emails, email)
  1276. subIDs = append(subIDs, client.SubID)
  1277. }
  1278. if len(prep) == 0 {
  1279. return result, false, nil
  1280. }
  1281. db := database.GetDB()
  1282. const lookupChunk = 400
  1283. existingByEmail := make(map[string]model.ClientRecord, len(emails))
  1284. for start := 0; start < len(emails); start += lookupChunk {
  1285. end := min(start+lookupChunk, len(emails))
  1286. var rows []model.ClientRecord
  1287. if e := db.Where("email IN ?", emails[start:end]).Find(&rows).Error; e != nil {
  1288. return result, false, e
  1289. }
  1290. for i := range rows {
  1291. existingByEmail[strings.ToLower(rows[i].Email)] = rows[i]
  1292. }
  1293. }
  1294. existingSubOwner := make(map[string]string, len(subIDs))
  1295. for start := 0; start < len(subIDs); start += lookupChunk {
  1296. end := min(start+lookupChunk, len(subIDs))
  1297. var rows []model.ClientRecord
  1298. if e := db.Where("sub_id IN ?", subIDs[start:end]).Find(&rows).Error; e != nil {
  1299. return result, false, e
  1300. }
  1301. for i := range rows {
  1302. existingSubOwner[rows[i].SubID] = strings.ToLower(rows[i].Email)
  1303. }
  1304. }
  1305. inboundCache := make(map[int]*model.Inbound)
  1306. getIb := func(id int) (*model.Inbound, error) {
  1307. if ib, ok := inboundCache[id]; ok {
  1308. return ib, nil
  1309. }
  1310. ib, e := inboundSvc.GetInbound(id)
  1311. if e != nil {
  1312. return nil, e
  1313. }
  1314. inboundCache[id] = ib
  1315. return ib, nil
  1316. }
  1317. byInbound := make(map[int][]model.Client)
  1318. idxByInbound := make(map[int][]int)
  1319. inboundOrder := make([]int, 0)
  1320. failed := make([]bool, len(prep))
  1321. reason := make([]string, len(prep))
  1322. createAnyTunnel := false
  1323. for idx := range prep {
  1324. le := strings.ToLower(prep[idx].client.Email)
  1325. if rec, ok := existingByEmail[le]; ok {
  1326. if rec.SubID != prep[idx].client.SubID {
  1327. failed[idx] = true
  1328. reason[idx] = "email already in use: " + prep[idx].client.Email
  1329. continue
  1330. }
  1331. if prep[idx].client.ID == "" {
  1332. prep[idx].client.ID = rec.UUID
  1333. }
  1334. if prep[idx].client.Password == "" {
  1335. prep[idx].client.Password = rec.Password
  1336. }
  1337. if prep[idx].client.Auth == "" {
  1338. prep[idx].client.Auth = rec.Auth
  1339. }
  1340. if prep[idx].client.Secret == "" {
  1341. prep[idx].client.Secret = rec.Secret
  1342. }
  1343. }
  1344. if owner, ok := existingSubOwner[prep[idx].client.SubID]; ok && owner != le {
  1345. failed[idx] = true
  1346. reason[idx] = "subId already in use: " + prep[idx].client.SubID
  1347. continue
  1348. }
  1349. ok := true
  1350. for _, ibId := range prep[idx].inboundIds {
  1351. ib, e := getIb(ibId)
  1352. if e != nil {
  1353. failed[idx] = true
  1354. reason[idx] = e.Error()
  1355. ok = false
  1356. break
  1357. }
  1358. if ib.Protocol == model.WireGuard || ib.Protocol == model.AmneziaWG {
  1359. createAnyTunnel = true
  1360. }
  1361. if e := s.fillProtocolDefaults(&prep[idx].client, ib); e != nil {
  1362. failed[idx] = true
  1363. reason[idx] = e.Error()
  1364. ok = false
  1365. break
  1366. }
  1367. }
  1368. if !ok {
  1369. continue
  1370. }
  1371. for _, ibId := range prep[idx].inboundIds {
  1372. ib, _ := getIb(ibId)
  1373. if _, seen := byInbound[ibId]; !seen {
  1374. inboundOrder = append(inboundOrder, ibId)
  1375. }
  1376. byInbound[ibId] = append(byInbound[ibId], clientWithInboundFlow(prep[idx].client, ib))
  1377. idxByInbound[ibId] = append(idxByInbound[ibId], idx)
  1378. }
  1379. }
  1380. needRestart := false
  1381. createResults, createPanics := fanoutInboundResults(inboundOrder, addFanoutLimit(createAnyTunnel), func(i int) inboundApplyOutcome {
  1382. ibId := inboundOrder[i]
  1383. payload, e := json.Marshal(map[string][]model.Client{"clients": byInbound[ibId]})
  1384. if e != nil {
  1385. return inboundApplyOutcome{err: e}
  1386. }
  1387. nr, e := s.AddInboundClient(inboundSvc, &model.Inbound{Id: ibId, Settings: string(payload)})
  1388. return inboundApplyOutcome{needRestart: nr, err: e}
  1389. })
  1390. for i, out := range createResults {
  1391. e := out.err
  1392. if createPanics[i] != nil {
  1393. // See BulkAttach: a panicking apply may already have committed.
  1394. needRestart = true
  1395. e = createPanics[i]
  1396. }
  1397. if e != nil {
  1398. for _, idx := range idxByInbound[inboundOrder[i]] {
  1399. failed[idx] = true
  1400. if reason[idx] == "" {
  1401. reason[idx] = e.Error()
  1402. }
  1403. }
  1404. continue
  1405. }
  1406. if out.needRestart {
  1407. needRestart = true
  1408. }
  1409. }
  1410. createdEmails := make([]string, 0, len(prep))
  1411. for idx := range prep {
  1412. if failed[idx] {
  1413. skip(prep[idx].client.Email, reason[idx])
  1414. continue
  1415. }
  1416. if err := s.setClientLimitHwidByEmail(nil, prep[idx].client.Email, prep[idx].limitHwid); err != nil {
  1417. skip(prep[idx].client.Email, err.Error())
  1418. continue
  1419. }
  1420. createdEmails = append(createdEmails, prep[idx].client.Email)
  1421. result.Created++
  1422. }
  1423. // A re-created email is a live identity again: a delete tombstone left
  1424. // standing makes the next node merge prune the new client's inbound links.
  1425. withdrawClientTombstones(createdEmails...)
  1426. return result, needRestart, nil
  1427. }
  1428. func (s *ClientService) DelDepleted(inboundSvc *InboundService) (int, bool, error) {
  1429. db := database.GetDB()
  1430. now := time.Now().UnixMilli()
  1431. depletedClause := depletedClientsClause
  1432. var rows []xray.ClientTraffic
  1433. if err := db.Where(depletedClause, now).Find(&rows).Error; err != nil {
  1434. return 0, false, err
  1435. }
  1436. if len(rows) == 0 {
  1437. return 0, false, nil
  1438. }
  1439. seen := make(map[string]struct{}, len(rows))
  1440. emails := make([]string, 0, len(rows))
  1441. for _, r := range rows {
  1442. if r.Email == "" {
  1443. continue
  1444. }
  1445. if _, ok := seen[r.Email]; ok {
  1446. continue
  1447. }
  1448. seen[r.Email] = struct{}{}
  1449. emails = append(emails, r.Email)
  1450. }
  1451. if len(emails) == 0 {
  1452. return 0, false, nil
  1453. }
  1454. res, needRestart, err := s.BulkDelete(inboundSvc, emails, false)
  1455. if err != nil {
  1456. return res.Deleted, needRestart, err
  1457. }
  1458. return res.Deleted, needRestart, nil
  1459. }
  1460. type BulkSetEnableResult struct {
  1461. Changed int `json:"changed"`
  1462. Skipped []BulkSetEnableReport `json:"skipped,omitempty"`
  1463. }
  1464. type BulkSetEnableReport struct {
  1465. Email string `json:"email"`
  1466. Reason string `json:"reason"`
  1467. }
  1468. func (s *ClientService) BulkSetEnable(inboundSvc *InboundService, emails []string, enable bool) (BulkSetEnableResult, bool, error) {
  1469. result := BulkSetEnableResult{}
  1470. seen := map[string]struct{}{}
  1471. cleanEmails := make([]string, 0, len(emails))
  1472. for _, e := range emails {
  1473. e = strings.TrimSpace(e)
  1474. if e == "" {
  1475. continue
  1476. }
  1477. if _, ok := seen[e]; ok {
  1478. continue
  1479. }
  1480. seen[e] = struct{}{}
  1481. cleanEmails = append(cleanEmails, e)
  1482. }
  1483. if len(cleanEmails) == 0 {
  1484. return result, false, nil
  1485. }
  1486. db := database.GetDB()
  1487. var records []model.ClientRecord
  1488. for _, batch := range chunkStrings(cleanEmails, sqlInChunk) {
  1489. var rows []model.ClientRecord
  1490. if err := db.Where("email IN ?", batch).Find(&rows).Error; err != nil {
  1491. return result, false, err
  1492. }
  1493. records = append(records, rows...)
  1494. }
  1495. recordsByEmail := make(map[string]*model.ClientRecord, len(records))
  1496. for i := range records {
  1497. recordsByEmail[records[i].Email] = &records[i]
  1498. }
  1499. skippedReasons := map[string]string{}
  1500. for _, email := range cleanEmails {
  1501. if _, ok := recordsByEmail[email]; !ok {
  1502. skippedReasons[email] = "client not found"
  1503. }
  1504. }
  1505. clientIds := make([]int, 0, len(recordsByEmail))
  1506. recordIdToEmail := make(map[int]string, len(recordsByEmail))
  1507. for _, r := range recordsByEmail {
  1508. clientIds = append(clientIds, r.Id)
  1509. recordIdToEmail[r.Id] = r.Email
  1510. }
  1511. emailsByInbound := map[int][]string{}
  1512. if len(clientIds) > 0 {
  1513. var mappings []model.ClientInbound
  1514. for _, batch := range chunkInts(clientIds, sqlInChunk) {
  1515. var rows []model.ClientInbound
  1516. if err := db.Where("client_id IN ?", batch).Find(&rows).Error; err != nil {
  1517. return result, false, err
  1518. }
  1519. mappings = append(mappings, rows...)
  1520. }
  1521. for _, m := range mappings {
  1522. email, ok := recordIdToEmail[m.ClientId]
  1523. if !ok {
  1524. continue
  1525. }
  1526. emailsByInbound[m.InboundId] = append(emailsByInbound[m.InboundId], email)
  1527. }
  1528. }
  1529. needRestart := false
  1530. enableIds := sortedInboundIds(emailsByInbound)
  1531. enableResults, enablePanics := fanoutInboundResults(enableIds, inboundFanoutConcurrency, func(i int) bulkSetEnableInboundResult {
  1532. return s.bulkSetEnableInboundClients(inboundSvc, enableIds[i], emailsByInbound[enableIds[i]], enable)
  1533. })
  1534. for i, ibRes := range enableResults {
  1535. if enablePanics[i] != nil {
  1536. needRestart = true
  1537. for _, email := range emailsByInbound[enableIds[i]] {
  1538. if _, already := skippedReasons[email]; !already {
  1539. skippedReasons[email] = enablePanics[i].Error()
  1540. }
  1541. }
  1542. continue
  1543. }
  1544. if ibRes.needRestart {
  1545. needRestart = true
  1546. }
  1547. for email, reason := range ibRes.perEmailSkipped {
  1548. if _, already := skippedReasons[email]; !already {
  1549. skippedReasons[email] = reason
  1550. }
  1551. }
  1552. }
  1553. successEmails := make([]string, 0, len(recordsByEmail))
  1554. for email := range recordsByEmail {
  1555. if _, skipped := skippedReasons[email]; skipped {
  1556. continue
  1557. }
  1558. successEmails = append(successEmails, email)
  1559. }
  1560. if len(successEmails) > 0 {
  1561. now := time.Now().UnixMilli()
  1562. if err := runSerializedTx(func(tx *gorm.DB) error {
  1563. for _, batch := range chunkStrings(successEmails, sqlInChunk) {
  1564. if e := tx.Model(xray.ClientTraffic{}).Where("email IN ?", batch).Update("enable", enable).Error; e != nil {
  1565. return e
  1566. }
  1567. if e := tx.Model(&model.ClientRecord{}).Where("email IN ?", batch).
  1568. Updates(map[string]any{"enable": enable, "updated_at": now}).Error; e != nil {
  1569. return e
  1570. }
  1571. }
  1572. return nil
  1573. }); err != nil {
  1574. return result, needRestart, err
  1575. }
  1576. }
  1577. result.Changed = len(successEmails)
  1578. for email, reason := range skippedReasons {
  1579. result.Skipped = append(result.Skipped, BulkSetEnableReport{Email: email, Reason: reason})
  1580. }
  1581. return result, needRestart, nil
  1582. }
  1583. type bulkSetEnableInboundResult struct {
  1584. perEmailSkipped map[string]string
  1585. needRestart bool
  1586. }
  1587. func (s *ClientService) bulkSetEnableInboundClients(inboundSvc *InboundService, inboundId int, emails []string, enable bool) bulkSetEnableInboundResult {
  1588. res := bulkSetEnableInboundResult{perEmailSkipped: map[string]string{}}
  1589. defer lockInbound(inboundId).Unlock()
  1590. oldInbound, err := inboundSvc.GetInbound(inboundId)
  1591. if err != nil {
  1592. for _, e := range emails {
  1593. res.perEmailSkipped[e] = err.Error()
  1594. }
  1595. return res
  1596. }
  1597. var settings map[string]any
  1598. if err := json.Unmarshal([]byte(oldInbound.Settings), &settings); err != nil {
  1599. for _, e := range emails {
  1600. res.perEmailSkipped[e] = err.Error()
  1601. }
  1602. return res
  1603. }
  1604. wanted := make(map[string]struct{}, len(emails))
  1605. for _, email := range emails {
  1606. wanted[email] = struct{}{}
  1607. }
  1608. cipher := ""
  1609. if oldInbound.Protocol == model.Shadowsocks {
  1610. cipher, _ = settings["method"].(string)
  1611. }
  1612. type changedClient struct {
  1613. email string
  1614. wasEnable bool
  1615. client model.Client
  1616. }
  1617. var changed []changedClient
  1618. found := map[string]bool{}
  1619. nowMs := time.Now().UnixMilli()
  1620. interfaceClients, _ := settings["clients"].([]any)
  1621. for i, c := range interfaceClients {
  1622. entry, ok := c.(map[string]any)
  1623. if !ok {
  1624. continue
  1625. }
  1626. email, _ := entry["email"].(string)
  1627. if _, want := wanted[email]; !want || email == "" {
  1628. continue
  1629. }
  1630. found[email] = true
  1631. prev, _ := entry["enable"].(bool)
  1632. if prev == enable {
  1633. continue
  1634. }
  1635. entry["enable"] = enable
  1636. entry["updated_at"] = nowMs
  1637. interfaceClients[i] = entry
  1638. // Build the pushed client from the inbound JSON (the per-inbound source of
  1639. // truth), so a remote UpdateUser carries every field and never zeroes
  1640. // subId/totalGB/expiry from drifting ClientRecord columns (#4628/#4792).
  1641. var parsed model.Client
  1642. if b, mErr := json.Marshal(entry); mErr == nil {
  1643. _ = json.Unmarshal(b, &parsed)
  1644. }
  1645. parsed.Email = email
  1646. parsed.Enable = enable
  1647. changed = append(changed, changedClient{email: email, wasEnable: prev, client: parsed})
  1648. }
  1649. for email := range wanted {
  1650. if !found[email] {
  1651. res.perEmailSkipped[email] = "Client Not Found In Inbound"
  1652. }
  1653. }
  1654. if len(changed) == 0 {
  1655. return res
  1656. }
  1657. settings["clients"] = interfaceClients
  1658. newSettings, err := json.MarshalIndent(settings, "", " ")
  1659. if err != nil {
  1660. for _, ch := range changed {
  1661. res.perEmailSkipped[ch.email] = err.Error()
  1662. }
  1663. return res
  1664. }
  1665. prevSettings := oldInbound.Settings
  1666. oldInbound.Settings = string(newSettings)
  1667. rt, push, _, perr := inboundSvc.nodePushPlan(oldInbound)
  1668. if perr != nil {
  1669. for _, ch := range changed {
  1670. res.perEmailSkipped[ch.email] = perr.Error()
  1671. }
  1672. return res
  1673. }
  1674. if oldInbound.NodeID != nil && push && len(changed) > nodeBulkPushThreshold {
  1675. push = false
  1676. }
  1677. txErr := runSerializedTx(func(tx *gorm.DB) error {
  1678. if e := tx.Save(oldInbound).Error; e != nil {
  1679. return e
  1680. }
  1681. finalClients, gcErr := inboundSvc.GetClients(oldInbound)
  1682. if gcErr != nil {
  1683. return gcErr
  1684. }
  1685. if err := s.SyncInbound(tx, inboundId, finalClients); err != nil {
  1686. return err
  1687. }
  1688. if oldInbound.NodeID != nil {
  1689. return (&NodeService{}).MarkNodeDirtyTx(tx, *oldInbound.NodeID)
  1690. }
  1691. return nil
  1692. })
  1693. if txErr != nil {
  1694. for _, ch := range changed {
  1695. res.perEmailSkipped[ch.email] = txErr.Error()
  1696. }
  1697. return res
  1698. }
  1699. if oldInbound.NodeID == nil {
  1700. if !push {
  1701. res.needRestart = true
  1702. } else {
  1703. for _, ch := range changed {
  1704. if enable {
  1705. err1 := rt.AddUser(context.Background(), oldInbound, map[string]any{
  1706. "email": ch.client.Email,
  1707. "id": ch.client.ID,
  1708. "security": ch.client.Security,
  1709. "flow": ch.client.Flow,
  1710. "auth": ch.client.Auth,
  1711. "password": ch.client.Password,
  1712. "cipher": cipher,
  1713. })
  1714. if err1 != nil {
  1715. logger.Debug("Error in adding client on", rt.Name(), ":", err1)
  1716. res.needRestart = true
  1717. }
  1718. } else if ch.wasEnable {
  1719. err1 := rt.RemoveUser(context.Background(), oldInbound, ch.email)
  1720. if err1 != nil && !strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", ch.email)) {
  1721. logger.Debug("Error in removing client on", rt.Name(), ":", err1)
  1722. res.needRestart = true
  1723. }
  1724. }
  1725. }
  1726. }
  1727. } else if push {
  1728. pushFailed := false
  1729. for _, ch := range changed {
  1730. updated := ch.client
  1731. updated.UpdatedAt = nowMs
  1732. ctx, cancel := nodePushContext()
  1733. err1 := rt.UpdateUser(ctx, oldInbound, ch.email, updated)
  1734. cancel()
  1735. if err1 != nil {
  1736. logger.Warning("Error in updating client on", rt.Name(), ":", err1)
  1737. pushFailed = true
  1738. // First failure ends the batch push; the reconcile converges the rest.
  1739. break
  1740. }
  1741. }
  1742. if !pushFailed {
  1743. advancePushedInbound(rt, prevSettings, oldInbound)
  1744. }
  1745. }
  1746. return res
  1747. }