user_handler.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. package handler
  2. import (
  3. "context"
  4. "strings"
  5. "time"
  6. "github.com/Wei-Shaw/sub2api/internal/handler/dto"
  7. "github.com/Wei-Shaw/sub2api/internal/handler/quotaview"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  9. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/gin-gonic/gin"
  12. )
  13. // UserHandler handles user-related requests
  14. type UserHandler struct {
  15. userService *service.UserService
  16. authService *service.AuthService
  17. emailService *service.EmailService
  18. emailCache service.EmailCache
  19. affiliateService *service.AffiliateService
  20. userPlatformQuotaRepo service.UserPlatformQuotaRepository
  21. }
  22. // NewUserHandler creates a new UserHandler
  23. func NewUserHandler(
  24. userService *service.UserService,
  25. authService *service.AuthService,
  26. emailService *service.EmailService,
  27. emailCache service.EmailCache,
  28. affiliateService *service.AffiliateService,
  29. userPlatformQuotaRepo service.UserPlatformQuotaRepository,
  30. ) *UserHandler {
  31. return &UserHandler{
  32. userService: userService,
  33. authService: authService,
  34. emailService: emailService,
  35. emailCache: emailCache,
  36. affiliateService: affiliateService,
  37. userPlatformQuotaRepo: userPlatformQuotaRepo,
  38. }
  39. }
  40. // GetMyPlatformQuotas GET /user/platform-quotas
  41. // 返回当前 JWT 用户的 platform quota 状态。
  42. // D14: 对每条记录逐档判断窗口过期,过期档位 usage=0、window_resets_at=null(不写 DB)
  43. func (h *UserHandler) GetMyPlatformQuotas(c *gin.Context) {
  44. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  45. if !ok {
  46. response.Unauthorized(c, "User not authenticated")
  47. return
  48. }
  49. if h.userPlatformQuotaRepo == nil {
  50. response.Success(c, map[string]any{"platform_quotas": []any{}})
  51. return
  52. }
  53. records, err := h.userPlatformQuotaRepo.ListByUser(c.Request.Context(), subject.UserID)
  54. if err != nil {
  55. response.ErrorFrom(c, err)
  56. return
  57. }
  58. now := time.Now().UTC()
  59. out := make([]map[string]any, 0, len(records))
  60. for _, r := range records {
  61. out = append(out, quotaview.LazyZeroQuotaForResponse(r, now, false))
  62. }
  63. response.Success(c, map[string]any{"platform_quotas": out})
  64. }
  65. // ChangePasswordRequest represents the change password request payload
  66. type ChangePasswordRequest struct {
  67. OldPassword string `json:"old_password" binding:"required"`
  68. NewPassword string `json:"new_password" binding:"required,min=6"`
  69. }
  70. // UpdateProfileRequest represents the update profile request payload
  71. type UpdateProfileRequest struct {
  72. Username *string `json:"username"`
  73. AvatarURL *string `json:"avatar_url"`
  74. BalanceNotifyEnabled *bool `json:"balance_notify_enabled"`
  75. BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"`
  76. }
  77. type userProfileResponse struct {
  78. dto.User
  79. AvatarURL string `json:"avatar_url,omitempty"`
  80. AvatarSource *userProfileSourceContext `json:"avatar_source,omitempty"`
  81. UsernameSource *userProfileSourceContext `json:"username_source,omitempty"`
  82. DisplayNameSource *userProfileSourceContext `json:"display_name_source,omitempty"`
  83. NicknameSource *userProfileSourceContext `json:"nickname_source,omitempty"`
  84. ProfileSources map[string]*userProfileSourceContext `json:"profile_sources,omitempty"`
  85. Identities service.UserIdentitySummarySet `json:"identities"`
  86. AuthBindings map[string]service.UserIdentitySummary `json:"auth_bindings"`
  87. IdentityBindings map[string]service.UserIdentitySummary `json:"identity_bindings"`
  88. EmailBound bool `json:"email_bound"`
  89. LinuxDoBound bool `json:"linuxdo_bound"`
  90. OIDCBound bool `json:"oidc_bound"`
  91. WeChatBound bool `json:"wechat_bound"`
  92. DingTalkBound bool `json:"dingtalk_bound"`
  93. }
  94. type userProfileSourceContext struct {
  95. Provider string `json:"provider,omitempty"`
  96. Source string `json:"source,omitempty"`
  97. }
  98. // GetProfile handles getting user profile
  99. // GET /api/v1/users/me
  100. func (h *UserHandler) GetProfile(c *gin.Context) {
  101. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  102. if !ok {
  103. response.Unauthorized(c, "User not authenticated")
  104. return
  105. }
  106. userData, err := h.userService.GetProfile(c.Request.Context(), subject.UserID)
  107. if err != nil {
  108. response.ErrorFrom(c, err)
  109. return
  110. }
  111. profileResp, err := h.buildUserProfileResponse(c.Request.Context(), subject.UserID, userData)
  112. if err != nil {
  113. response.ErrorFrom(c, err)
  114. return
  115. }
  116. response.Success(c, profileResp)
  117. }
  118. // ChangePassword handles changing user password
  119. // POST /api/v1/users/me/password
  120. func (h *UserHandler) ChangePassword(c *gin.Context) {
  121. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  122. if !ok {
  123. response.Unauthorized(c, "User not authenticated")
  124. return
  125. }
  126. var req ChangePasswordRequest
  127. if err := c.ShouldBindJSON(&req); err != nil {
  128. response.BadRequest(c, "Invalid request: "+err.Error())
  129. return
  130. }
  131. svcReq := service.ChangePasswordRequest{
  132. CurrentPassword: req.OldPassword,
  133. NewPassword: req.NewPassword,
  134. }
  135. err := h.userService.ChangePassword(c.Request.Context(), subject.UserID, svcReq)
  136. if err != nil {
  137. response.ErrorFrom(c, err)
  138. return
  139. }
  140. response.Success(c, gin.H{"message": "Password changed successfully"})
  141. }
  142. // UpdateProfile handles updating user profile
  143. // PUT /api/v1/users/me
  144. func (h *UserHandler) UpdateProfile(c *gin.Context) {
  145. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  146. if !ok {
  147. response.Unauthorized(c, "User not authenticated")
  148. return
  149. }
  150. var req UpdateProfileRequest
  151. if err := c.ShouldBindJSON(&req); err != nil {
  152. response.BadRequest(c, "Invalid request: "+err.Error())
  153. return
  154. }
  155. svcReq := service.UpdateProfileRequest{
  156. Username: req.Username,
  157. AvatarURL: req.AvatarURL,
  158. BalanceNotifyEnabled: req.BalanceNotifyEnabled,
  159. BalanceNotifyThreshold: req.BalanceNotifyThreshold,
  160. }
  161. updatedUser, err := h.userService.UpdateProfile(c.Request.Context(), subject.UserID, svcReq)
  162. if err != nil {
  163. response.ErrorFrom(c, err)
  164. return
  165. }
  166. profileResp, err := h.buildUserProfileResponse(c.Request.Context(), subject.UserID, updatedUser)
  167. if err != nil {
  168. response.ErrorFrom(c, err)
  169. return
  170. }
  171. response.Success(c, profileResp)
  172. }
  173. // GetAffiliate returns the current user's affiliate details.
  174. // GET /api/v1/user/aff
  175. func (h *UserHandler) GetAffiliate(c *gin.Context) {
  176. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  177. if !ok {
  178. response.Unauthorized(c, "User not authenticated")
  179. return
  180. }
  181. detail, err := h.affiliateService.GetAffiliateDetail(c.Request.Context(), subject.UserID)
  182. if err != nil {
  183. response.ErrorFrom(c, err)
  184. return
  185. }
  186. response.Success(c, detail)
  187. }
  188. // TransferAffiliateQuota transfers all available affiliate quota into current balance.
  189. // POST /api/v1/user/aff/transfer
  190. func (h *UserHandler) TransferAffiliateQuota(c *gin.Context) {
  191. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  192. if !ok {
  193. response.Unauthorized(c, "User not authenticated")
  194. return
  195. }
  196. transferred, balance, err := h.affiliateService.TransferAffiliateQuota(c.Request.Context(), subject.UserID)
  197. if err != nil {
  198. response.ErrorFrom(c, err)
  199. return
  200. }
  201. response.Success(c, gin.H{
  202. "transferred_quota": transferred,
  203. "balance": balance,
  204. })
  205. }
  206. type StartIdentityBindingRequest struct {
  207. Provider string `json:"provider" binding:"required"`
  208. RedirectTo string `json:"redirect_to"`
  209. }
  210. type BindEmailIdentityRequest struct {
  211. Email string `json:"email" binding:"required,email"`
  212. VerifyCode string `json:"verify_code" binding:"required"`
  213. Password string `json:"password" binding:"required"`
  214. }
  215. type SendEmailBindingCodeRequest struct {
  216. Email string `json:"email" binding:"required,email"`
  217. }
  218. // StartIdentityBinding returns the backend authorize URL for starting a third-party identity bind flow.
  219. // POST /api/v1/user/auth-identities/bind/start
  220. func (h *UserHandler) StartIdentityBinding(c *gin.Context) {
  221. if _, ok := middleware2.GetAuthSubjectFromContext(c); !ok {
  222. response.Unauthorized(c, "User not authenticated")
  223. return
  224. }
  225. var req StartIdentityBindingRequest
  226. if err := c.ShouldBindJSON(&req); err != nil {
  227. response.BadRequest(c, "Invalid request: "+err.Error())
  228. return
  229. }
  230. result, err := h.userService.PrepareIdentityBindingStart(c.Request.Context(), service.StartUserIdentityBindingRequest{
  231. Provider: req.Provider,
  232. RedirectTo: req.RedirectTo,
  233. })
  234. if err != nil {
  235. response.ErrorFrom(c, err)
  236. return
  237. }
  238. response.Success(c, result)
  239. }
  240. // BindEmailIdentity verifies and binds a local email identity for the current user.
  241. // POST /api/v1/user/account-bindings/email
  242. func (h *UserHandler) BindEmailIdentity(c *gin.Context) {
  243. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  244. if !ok {
  245. response.Unauthorized(c, "User not authenticated")
  246. return
  247. }
  248. if h.authService == nil {
  249. response.InternalError(c, "Auth service not configured")
  250. return
  251. }
  252. var req BindEmailIdentityRequest
  253. if err := c.ShouldBindJSON(&req); err != nil {
  254. response.BadRequest(c, "Invalid request: "+err.Error())
  255. return
  256. }
  257. updatedUser, err := h.authService.BindEmailIdentity(
  258. c.Request.Context(),
  259. subject.UserID,
  260. req.Email,
  261. req.VerifyCode,
  262. req.Password,
  263. )
  264. if err != nil {
  265. response.ErrorFrom(c, err)
  266. return
  267. }
  268. profileResp, err := h.buildUserProfileResponse(c.Request.Context(), subject.UserID, updatedUser)
  269. if err != nil {
  270. response.ErrorFrom(c, err)
  271. return
  272. }
  273. response.Success(c, profileResp)
  274. }
  275. // UnbindIdentity removes a third-party sign-in provider from the current user.
  276. // DELETE /api/v1/user/account-bindings/:provider
  277. func (h *UserHandler) UnbindIdentity(c *gin.Context) {
  278. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  279. if !ok {
  280. response.Unauthorized(c, "User not authenticated")
  281. return
  282. }
  283. updatedUser, unbound, err := h.userService.UnbindUserAuthProviderWithResult(
  284. c.Request.Context(),
  285. subject.UserID,
  286. c.Param("provider"),
  287. )
  288. if err != nil {
  289. response.ErrorFrom(c, err)
  290. return
  291. }
  292. if unbound && h.authService != nil {
  293. if err := h.authService.RevokeAllUserTokens(c.Request.Context(), subject.UserID); err != nil {
  294. response.ErrorFrom(c, err)
  295. return
  296. }
  297. }
  298. profileResp, err := h.buildUserProfileResponse(c.Request.Context(), subject.UserID, updatedUser)
  299. if err != nil {
  300. response.ErrorFrom(c, err)
  301. return
  302. }
  303. response.Success(c, profileResp)
  304. }
  305. // SendEmailBindingCode sends a verification code for the current user's email binding flow.
  306. // POST /api/v1/user/account-bindings/email/send-code
  307. func (h *UserHandler) SendEmailBindingCode(c *gin.Context) {
  308. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  309. if !ok {
  310. response.Unauthorized(c, "User not authenticated")
  311. return
  312. }
  313. if h.authService == nil {
  314. response.InternalError(c, "Auth service not configured")
  315. return
  316. }
  317. var req SendEmailBindingCodeRequest
  318. if err := c.ShouldBindJSON(&req); err != nil {
  319. response.BadRequest(c, "Invalid request: "+err.Error())
  320. return
  321. }
  322. if err := h.authService.SendEmailIdentityBindCode(c.Request.Context(), subject.UserID, req.Email, c.GetHeader("Accept-Language")); err != nil {
  323. response.ErrorFrom(c, err)
  324. return
  325. }
  326. response.Success(c, gin.H{"message": "Verification code sent successfully"})
  327. }
  328. // SendNotifyEmailCodeRequest represents the request to send notify email verification code
  329. type SendNotifyEmailCodeRequest struct {
  330. Email string `json:"email" binding:"required,email"`
  331. }
  332. // SendNotifyEmailCode sends verification code to extra notification email
  333. // POST /api/v1/user/notify-email/send-code
  334. func (h *UserHandler) SendNotifyEmailCode(c *gin.Context) {
  335. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  336. if !ok {
  337. response.Unauthorized(c, "User not authenticated")
  338. return
  339. }
  340. var req SendNotifyEmailCodeRequest
  341. if err := c.ShouldBindJSON(&req); err != nil {
  342. response.BadRequest(c, "Invalid request: "+err.Error())
  343. return
  344. }
  345. err := h.userService.SendNotifyEmailCode(c.Request.Context(), subject.UserID, req.Email, h.emailService, h.emailCache, c.GetHeader("Accept-Language"))
  346. if err != nil {
  347. response.ErrorFrom(c, err)
  348. return
  349. }
  350. response.Success(c, gin.H{"message": "Verification code sent successfully"})
  351. }
  352. // VerifyNotifyEmailRequest represents the request to verify and add notify email
  353. type VerifyNotifyEmailRequest struct {
  354. Email string `json:"email" binding:"required,email"`
  355. Code string `json:"code" binding:"required,len=6"`
  356. }
  357. // VerifyNotifyEmail verifies code and adds email to notification list
  358. // POST /api/v1/user/notify-email/verify
  359. func (h *UserHandler) VerifyNotifyEmail(c *gin.Context) {
  360. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  361. if !ok {
  362. response.Unauthorized(c, "User not authenticated")
  363. return
  364. }
  365. var req VerifyNotifyEmailRequest
  366. if err := c.ShouldBindJSON(&req); err != nil {
  367. response.BadRequest(c, "Invalid request: "+err.Error())
  368. return
  369. }
  370. err := h.userService.VerifyAndAddNotifyEmail(c.Request.Context(), subject.UserID, req.Email, req.Code, h.emailCache)
  371. if err != nil {
  372. response.ErrorFrom(c, err)
  373. return
  374. }
  375. // Return updated user
  376. updatedUser, err := h.userService.GetByID(c.Request.Context(), subject.UserID)
  377. if err != nil {
  378. response.ErrorFrom(c, err)
  379. return
  380. }
  381. profileResp, err := h.buildUserProfileResponse(c.Request.Context(), subject.UserID, updatedUser)
  382. if err != nil {
  383. response.ErrorFrom(c, err)
  384. return
  385. }
  386. response.Success(c, profileResp)
  387. }
  388. // RemoveNotifyEmailRequest represents the request to remove a notify email
  389. type RemoveNotifyEmailRequest struct {
  390. Email string `json:"email" binding:"required,email"`
  391. }
  392. // RemoveNotifyEmail removes email from notification list
  393. // DELETE /api/v1/user/notify-email
  394. func (h *UserHandler) RemoveNotifyEmail(c *gin.Context) {
  395. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  396. if !ok {
  397. response.Unauthorized(c, "User not authenticated")
  398. return
  399. }
  400. var req RemoveNotifyEmailRequest
  401. if err := c.ShouldBindJSON(&req); err != nil {
  402. response.BadRequest(c, "Invalid request: "+err.Error())
  403. return
  404. }
  405. err := h.userService.RemoveNotifyEmail(c.Request.Context(), subject.UserID, req.Email)
  406. if err != nil {
  407. response.ErrorFrom(c, err)
  408. return
  409. }
  410. // Return updated user
  411. updatedUser, err := h.userService.GetByID(c.Request.Context(), subject.UserID)
  412. if err != nil {
  413. response.ErrorFrom(c, err)
  414. return
  415. }
  416. profileResp, err := h.buildUserProfileResponse(c.Request.Context(), subject.UserID, updatedUser)
  417. if err != nil {
  418. response.ErrorFrom(c, err)
  419. return
  420. }
  421. response.Success(c, profileResp)
  422. }
  423. // ToggleNotifyEmailRequest represents the request to toggle a notify email's disabled state
  424. type ToggleNotifyEmailRequest struct {
  425. Email string `json:"email" binding:"required,email"`
  426. Disabled bool `json:"disabled"`
  427. }
  428. // ToggleNotifyEmail toggles the disabled state of a notification email
  429. // PUT /api/v1/user/notify-email/toggle
  430. func (h *UserHandler) ToggleNotifyEmail(c *gin.Context) {
  431. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  432. if !ok {
  433. response.Unauthorized(c, "User not authenticated")
  434. return
  435. }
  436. var req ToggleNotifyEmailRequest
  437. if err := c.ShouldBindJSON(&req); err != nil {
  438. response.BadRequest(c, "Invalid request: "+err.Error())
  439. return
  440. }
  441. err := h.userService.ToggleNotifyEmail(c.Request.Context(), subject.UserID, req.Email, req.Disabled)
  442. if err != nil {
  443. response.ErrorFrom(c, err)
  444. return
  445. }
  446. updatedUser, err := h.userService.GetByID(c.Request.Context(), subject.UserID)
  447. if err != nil {
  448. response.ErrorFrom(c, err)
  449. return
  450. }
  451. profileResp, err := h.buildUserProfileResponse(c.Request.Context(), subject.UserID, updatedUser)
  452. if err != nil {
  453. response.ErrorFrom(c, err)
  454. return
  455. }
  456. response.Success(c, profileResp)
  457. }
  458. func (h *UserHandler) buildUserProfileResponse(ctx context.Context, userID int64, user *service.User) (userProfileResponse, error) {
  459. identities, err := h.userService.GetProfileIdentitySummaries(ctx, userID, user)
  460. if err != nil {
  461. return userProfileResponse{}, err
  462. }
  463. return userProfileResponseFromService(user, identities), nil
  464. }
  465. func userProfileResponseFromService(user *service.User, identities service.UserIdentitySummarySet) userProfileResponse {
  466. base := dto.UserFromService(user)
  467. if base == nil {
  468. return userProfileResponse{}
  469. }
  470. bindings := userProfileBindingMap(identities)
  471. profileSources, avatarSource, usernameSource := inferUserProfileSources(user, identities)
  472. return userProfileResponse{
  473. User: *base,
  474. AvatarURL: user.AvatarURL,
  475. AvatarSource: avatarSource,
  476. UsernameSource: usernameSource,
  477. DisplayNameSource: usernameSource,
  478. NicknameSource: usernameSource,
  479. ProfileSources: profileSources,
  480. Identities: identities,
  481. AuthBindings: bindings,
  482. IdentityBindings: bindings,
  483. EmailBound: identities.Email.Bound,
  484. LinuxDoBound: identities.LinuxDo.Bound,
  485. OIDCBound: identities.OIDC.Bound,
  486. WeChatBound: identities.WeChat.Bound,
  487. DingTalkBound: identities.DingTalk.Bound,
  488. }
  489. }
  490. func userProfileBindingMap(identities service.UserIdentitySummarySet) map[string]service.UserIdentitySummary {
  491. return map[string]service.UserIdentitySummary{
  492. "email": identities.Email,
  493. "linuxdo": identities.LinuxDo,
  494. "oidc": identities.OIDC,
  495. "wechat": identities.WeChat,
  496. "dingtalk": identities.DingTalk,
  497. }
  498. }
  499. func inferUserProfileSources(user *service.User, identities service.UserIdentitySummarySet) (
  500. map[string]*userProfileSourceContext,
  501. *userProfileSourceContext,
  502. *userProfileSourceContext,
  503. ) {
  504. if user == nil {
  505. return nil, nil, nil
  506. }
  507. thirdParty := thirdPartyIdentityProviders(identities)
  508. var avatarSource *userProfileSourceContext
  509. avatarValue := strings.TrimSpace(user.AvatarURL)
  510. for _, summary := range thirdParty {
  511. if avatarValue != "" && avatarValue == strings.TrimSpace(summary.AvatarURL) {
  512. avatarSource = buildUserProfileSourceContext(summary.Provider)
  513. break
  514. }
  515. }
  516. usernameValue := strings.TrimSpace(user.Username)
  517. var usernameSource *userProfileSourceContext
  518. for _, summary := range thirdParty {
  519. if usernameValue != "" && usernameValue == strings.TrimSpace(summary.DisplayName) {
  520. usernameSource = buildUserProfileSourceContext(summary.Provider)
  521. break
  522. }
  523. }
  524. profileSources := map[string]*userProfileSourceContext{}
  525. if avatarSource != nil {
  526. profileSources["avatar"] = avatarSource
  527. }
  528. if usernameSource != nil {
  529. profileSources["username"] = usernameSource
  530. profileSources["display_name"] = usernameSource
  531. profileSources["nickname"] = usernameSource
  532. }
  533. if len(profileSources) == 0 {
  534. return nil, avatarSource, usernameSource
  535. }
  536. return profileSources, avatarSource, usernameSource
  537. }
  538. func thirdPartyIdentityProviders(identities service.UserIdentitySummarySet) []service.UserIdentitySummary {
  539. out := make([]service.UserIdentitySummary, 0, 3)
  540. for _, summary := range []service.UserIdentitySummary{identities.LinuxDo, identities.OIDC, identities.WeChat, identities.DingTalk} {
  541. if summary.Bound {
  542. out = append(out, summary)
  543. }
  544. }
  545. return out
  546. }
  547. func buildUserProfileSourceContext(provider string) *userProfileSourceContext {
  548. provider = strings.TrimSpace(provider)
  549. if provider == "" {
  550. return nil
  551. }
  552. return &userProfileSourceContext{
  553. Provider: provider,
  554. Source: provider,
  555. }
  556. }