auth_handler.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. package handler
  2. import (
  3. "context"
  4. "log/slog"
  5. "strings"
  6. "sync"
  7. "github.com/Wei-Shaw/sub2api/internal/config"
  8. "github.com/Wei-Shaw/sub2api/internal/handler/dto"
  9. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/ip"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  12. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  13. "github.com/Wei-Shaw/sub2api/internal/service"
  14. "github.com/gin-gonic/gin"
  15. )
  16. // AuthHandler handles authentication-related requests
  17. type AuthHandler struct {
  18. cfg *config.Config
  19. authService *service.AuthService
  20. userService *service.UserService
  21. settingSvc *service.SettingService
  22. promoService *service.PromoService
  23. redeemService *service.RedeemService
  24. totpService *service.TotpService
  25. userAttributeService *service.UserAttributeService
  26. dingTalkClientInstance *DingTalkClient
  27. dingTalkClientMu sync.Mutex
  28. }
  29. // NewAuthHandler creates a new AuthHandler
  30. func NewAuthHandler(cfg *config.Config, authService *service.AuthService, userService *service.UserService, settingService *service.SettingService, promoService *service.PromoService, redeemService *service.RedeemService, totpService *service.TotpService, userAttributeService *service.UserAttributeService) *AuthHandler {
  31. return &AuthHandler{
  32. cfg: cfg,
  33. authService: authService,
  34. userService: userService,
  35. settingSvc: settingService,
  36. promoService: promoService,
  37. redeemService: redeemService,
  38. totpService: totpService,
  39. userAttributeService: userAttributeService,
  40. }
  41. }
  42. // RegisterRequest represents the registration request payload
  43. type RegisterRequest struct {
  44. Email string `json:"email" binding:"required,email"`
  45. Password string `json:"password" binding:"required,min=6"`
  46. VerifyCode string `json:"verify_code"`
  47. TurnstileToken string `json:"turnstile_token"`
  48. TencentCaptchaTicket string `json:"tencent_captcha_ticket"`
  49. TencentCaptchaRandstr string `json:"tencent_captcha_randstr"`
  50. PromoCode string `json:"promo_code"` // 注册优惠码
  51. InvitationCode string `json:"invitation_code"` // 邀请码
  52. AffCode string `json:"aff_code"` // 邀请返利码
  53. }
  54. // SendVerifyCodeRequest 发送验证码请求
  55. type SendVerifyCodeRequest struct {
  56. Email string `json:"email" binding:"required,email"`
  57. TurnstileToken string `json:"turnstile_token"`
  58. TencentCaptchaTicket string `json:"tencent_captcha_ticket"`
  59. TencentCaptchaRandstr string `json:"tencent_captcha_randstr"`
  60. }
  61. // SendVerifyCodeResponse 发送验证码响应
  62. type SendVerifyCodeResponse struct {
  63. Message string `json:"message"`
  64. Countdown int `json:"countdown"` // 倒计时秒数
  65. }
  66. // LoginRequest represents the login request payload
  67. type LoginRequest struct {
  68. Email string `json:"email" binding:"required,email"`
  69. Password string `json:"password" binding:"required"`
  70. TurnstileToken string `json:"turnstile_token"`
  71. TencentCaptchaTicket string `json:"tencent_captcha_ticket"`
  72. TencentCaptchaRandstr string `json:"tencent_captcha_randstr"`
  73. }
  74. func captchaProof(turnstileToken, tencentTicket, tencentRandstr string) service.CaptchaProof {
  75. return service.CaptchaProof{
  76. TurnstileToken: turnstileToken,
  77. TencentTicket: tencentTicket,
  78. TencentRandstr: tencentRandstr,
  79. }
  80. }
  81. // AuthResponse 认证响应格式(匹配前端期望)
  82. type AuthResponse struct {
  83. AccessToken string `json:"access_token"`
  84. RefreshToken string `json:"refresh_token,omitempty"` // 新增:Refresh Token
  85. ExpiresIn int `json:"expires_in,omitempty"` // 新增:Access Token有效期(秒)
  86. TokenType string `json:"token_type"`
  87. User *dto.User `json:"user"`
  88. }
  89. func ensureLoginUserActive(user *service.User) error {
  90. if user == nil {
  91. return infraerrors.Unauthorized("INVALID_USER", "user not found")
  92. }
  93. if !user.IsActive() {
  94. return service.ErrUserNotActive
  95. }
  96. return nil
  97. }
  98. // respondWithTokenPair 生成 Token 对并返回认证响应
  99. // 如果 Token 对生成失败,回退到只返回 Access Token(向后兼容)
  100. func (h *AuthHandler) respondWithTokenPair(c *gin.Context, user *service.User) {
  101. respondWithTokenPair(c, h.authService, user)
  102. }
  103. func respondWithTokenPair(c *gin.Context, authService *service.AuthService, user *service.User) {
  104. if err := ensureLoginUserActive(user); err != nil {
  105. response.ErrorFrom(c, err)
  106. return
  107. }
  108. tokenPair, err := authService.GenerateTokenPair(c.Request.Context(), user, "")
  109. if err != nil {
  110. slog.Error("failed to generate token pair", "error", err, "user_id", user.ID)
  111. // 回退到只返回Access Token
  112. token, tokenErr := authService.GenerateToken(c.Request.Context(), user)
  113. if tokenErr != nil {
  114. response.InternalError(c, "Failed to generate token")
  115. return
  116. }
  117. response.Success(c, AuthResponse{
  118. AccessToken: token,
  119. TokenType: "Bearer",
  120. User: dto.UserFromService(user),
  121. })
  122. return
  123. }
  124. response.Success(c, AuthResponse{
  125. AccessToken: tokenPair.AccessToken,
  126. RefreshToken: tokenPair.RefreshToken,
  127. ExpiresIn: tokenPair.ExpiresIn,
  128. TokenType: "Bearer",
  129. User: dto.UserFromService(user),
  130. })
  131. }
  132. func (h *AuthHandler) ensureBackendModeAllowsUser(ctx context.Context, user *service.User) error {
  133. if user == nil {
  134. return infraerrors.Unauthorized("INVALID_USER", "user not found")
  135. }
  136. if h == nil || !h.isBackendModeEnabled(ctx) || user.IsAdmin() {
  137. return nil
  138. }
  139. return infraerrors.Forbidden("BACKEND_MODE_ADMIN_ONLY", "Backend mode is active. Only admin login is allowed.")
  140. }
  141. func (h *AuthHandler) ensureBackendModeAllowsNewUserLogin(ctx context.Context) error {
  142. if h == nil || !h.isBackendModeEnabled(ctx) {
  143. return nil
  144. }
  145. return infraerrors.Forbidden("BACKEND_MODE_ADMIN_ONLY", "Backend mode is active. Only admin login is allowed.")
  146. }
  147. func (h *AuthHandler) isBackendModeEnabled(ctx context.Context) bool {
  148. if h == nil || h.settingSvc == nil {
  149. return false
  150. }
  151. settings, err := h.settingSvc.GetPublicSettings(ctx)
  152. if err == nil && settings != nil {
  153. return settings.BackendModeEnabled
  154. }
  155. return h.settingSvc.IsBackendModeEnabled(ctx)
  156. }
  157. // Register handles user registration
  158. // POST /api/v1/auth/register
  159. func (h *AuthHandler) Register(c *gin.Context) {
  160. var req RegisterRequest
  161. if err := c.ShouldBindJSON(&req); err != nil {
  162. response.BadRequest(c, "Invalid request: "+err.Error())
  163. return
  164. }
  165. // 验证当前启用的验证码(邮箱验证码注册场景避免重复校验一次性票据)
  166. proof := captchaProof(req.TurnstileToken, req.TencentCaptchaTicket, req.TencentCaptchaRandstr)
  167. if err := h.authService.VerifyCaptchaForRegister(c.Request.Context(), proof, ip.GetClientIP(c), req.VerifyCode); err != nil {
  168. response.ErrorFrom(c, err)
  169. return
  170. }
  171. _, user, err := h.authService.RegisterWithVerification(
  172. c.Request.Context(),
  173. req.Email,
  174. req.Password,
  175. req.VerifyCode,
  176. req.PromoCode,
  177. req.InvitationCode,
  178. req.AffCode,
  179. )
  180. if err != nil {
  181. response.ErrorFrom(c, err)
  182. return
  183. }
  184. h.respondWithTokenPair(c, user)
  185. }
  186. // SendVerifyCode 发送邮箱验证码
  187. // POST /api/v1/auth/send-verify-code
  188. func (h *AuthHandler) SendVerifyCode(c *gin.Context) {
  189. var req SendVerifyCodeRequest
  190. if err := c.ShouldBindJSON(&req); err != nil {
  191. response.BadRequest(c, "Invalid request: "+err.Error())
  192. return
  193. }
  194. proof := captchaProof(req.TurnstileToken, req.TencentCaptchaTicket, req.TencentCaptchaRandstr)
  195. if err := h.authService.VerifyCaptcha(c.Request.Context(), proof, ip.GetClientIP(c)); err != nil {
  196. response.ErrorFrom(c, err)
  197. return
  198. }
  199. result, err := h.authService.SendVerifyCodeAsync(c.Request.Context(), req.Email, c.GetHeader("Accept-Language"))
  200. if err != nil {
  201. response.ErrorFrom(c, err)
  202. return
  203. }
  204. response.Success(c, SendVerifyCodeResponse{
  205. Message: "Verification code sent successfully",
  206. Countdown: result.Countdown,
  207. })
  208. }
  209. // Login handles user login
  210. // POST /api/v1/auth/login
  211. func (h *AuthHandler) Login(c *gin.Context) {
  212. var req LoginRequest
  213. if err := c.ShouldBindJSON(&req); err != nil {
  214. response.BadRequest(c, "Invalid request: "+err.Error())
  215. return
  216. }
  217. proof := captchaProof(req.TurnstileToken, req.TencentCaptchaTicket, req.TencentCaptchaRandstr)
  218. if err := h.authService.VerifyCaptcha(c.Request.Context(), proof, ip.GetClientIP(c)); err != nil {
  219. response.ErrorFrom(c, err)
  220. return
  221. }
  222. token, user, err := h.authService.Login(c.Request.Context(), req.Email, req.Password)
  223. if err != nil {
  224. response.ErrorFrom(c, err)
  225. return
  226. }
  227. _ = token // token 由 authService.Login 返回但此处由 respondWithTokenPair 重新生成
  228. if err := h.ensureBackendModeAllowsUser(c.Request.Context(), user); err != nil {
  229. response.ErrorFrom(c, err)
  230. return
  231. }
  232. // Check if TOTP 2FA is enabled for this user
  233. if h.totpService != nil && h.settingSvc.IsTotpEnabled(c.Request.Context()) && user.TotpEnabled {
  234. // Create a temporary login session for 2FA
  235. tempToken, err := h.totpService.CreateLoginSession(c.Request.Context(), user.ID, user.Email)
  236. if err != nil {
  237. response.InternalError(c, "Failed to create 2FA session")
  238. return
  239. }
  240. response.Success(c, TotpLoginResponse{
  241. Requires2FA: true,
  242. TempToken: tempToken,
  243. UserEmailMasked: service.MaskEmail(user.Email),
  244. })
  245. return
  246. }
  247. h.authService.RecordSuccessfulLogin(c.Request.Context(), user.ID)
  248. h.respondWithTokenPair(c, user)
  249. }
  250. // TotpLoginResponse represents the response when 2FA is required
  251. type TotpLoginResponse struct {
  252. Requires2FA bool `json:"requires_2fa"`
  253. TempToken string `json:"temp_token,omitempty"`
  254. UserEmailMasked string `json:"user_email_masked,omitempty"`
  255. }
  256. // Login2FARequest represents the 2FA login request
  257. type Login2FARequest struct {
  258. TempToken string `json:"temp_token" binding:"required"`
  259. TotpCode string `json:"totp_code" binding:"required,len=6"`
  260. }
  261. // Login2FA completes the login with 2FA verification
  262. // POST /api/v1/auth/login/2fa
  263. func (h *AuthHandler) Login2FA(c *gin.Context) {
  264. var req Login2FARequest
  265. if err := c.ShouldBindJSON(&req); err != nil {
  266. response.BadRequest(c, "Invalid request: "+err.Error())
  267. return
  268. }
  269. slog.Debug("login_2fa_request",
  270. "temp_token_len", len(req.TempToken),
  271. "totp_code_len", len(req.TotpCode))
  272. // Get the login session
  273. session, err := h.totpService.GetLoginSession(c.Request.Context(), req.TempToken)
  274. if err != nil || session == nil {
  275. tokenPrefix := ""
  276. if len(req.TempToken) >= 8 {
  277. tokenPrefix = req.TempToken[:8]
  278. }
  279. slog.Debug("login_2fa_session_invalid",
  280. "temp_token_prefix", tokenPrefix,
  281. "error", err)
  282. response.BadRequest(c, "Invalid or expired 2FA session")
  283. return
  284. }
  285. slog.Debug("login_2fa_session_found",
  286. "user_id", session.UserID,
  287. "email", session.Email)
  288. // Verify the TOTP code
  289. if err := h.totpService.VerifyCode(c.Request.Context(), session.UserID, req.TotpCode); err != nil {
  290. slog.Debug("login_2fa_verify_failed",
  291. "user_id", session.UserID,
  292. "error", err)
  293. response.ErrorFrom(c, err)
  294. return
  295. }
  296. // Get the user (before session deletion so we can check backend mode)
  297. user, err := h.userService.GetByID(c.Request.Context(), session.UserID)
  298. if err != nil {
  299. response.ErrorFrom(c, err)
  300. return
  301. }
  302. if err := ensureLoginUserActive(user); err != nil {
  303. response.ErrorFrom(c, err)
  304. return
  305. }
  306. if err := h.ensureBackendModeAllowsUser(c.Request.Context(), user); err != nil {
  307. response.ErrorFrom(c, err)
  308. return
  309. }
  310. if session.PendingOAuthBind != nil {
  311. pendingSvc, err := h.pendingIdentityService()
  312. if err != nil {
  313. response.ErrorFrom(c, err)
  314. return
  315. }
  316. pendingSession, err := pendingSvc.GetBrowserSession(
  317. c.Request.Context(),
  318. session.PendingOAuthBind.PendingSessionToken,
  319. session.PendingOAuthBind.BrowserSessionKey,
  320. )
  321. if err != nil {
  322. response.ErrorFrom(c, err)
  323. return
  324. }
  325. decision, err := h.ensurePendingOAuthAdoptionDecision(c, pendingSession.ID, oauthAdoptionDecisionRequest{})
  326. if err != nil {
  327. response.ErrorFrom(c, err)
  328. return
  329. }
  330. if err := applyPendingOAuthBinding(
  331. c.Request.Context(),
  332. h.entClient(),
  333. h.authService,
  334. h.userService,
  335. pendingSession,
  336. decision,
  337. &user.ID,
  338. true,
  339. true,
  340. ); err != nil {
  341. response.ErrorFrom(c, infraerrors.InternalServer("PENDING_AUTH_BIND_APPLY_FAILED", "failed to bind pending oauth identity").WithCause(err))
  342. return
  343. }
  344. if _, err := pendingSvc.ConsumeBrowserSession(
  345. c.Request.Context(),
  346. pendingSession.SessionToken,
  347. pendingSession.BrowserSessionKey,
  348. ); err != nil {
  349. response.ErrorFrom(c, err)
  350. return
  351. }
  352. secureCookie := isRequestHTTPS(c)
  353. clearOAuthPendingSessionCookie(c, secureCookie)
  354. clearOAuthPendingBrowserCookie(c, secureCookie)
  355. h.authService.RecordSuccessfulLogin(c.Request.Context(), user.ID)
  356. user, err = h.userService.GetByID(c.Request.Context(), session.UserID)
  357. if err != nil {
  358. response.ErrorFrom(c, err)
  359. return
  360. }
  361. }
  362. // Delete the login session (only after all checks pass)
  363. _ = h.totpService.DeleteLoginSession(c.Request.Context(), req.TempToken)
  364. if session.PendingOAuthBind == nil {
  365. h.authService.RecordSuccessfulLogin(c.Request.Context(), user.ID)
  366. }
  367. h.respondWithTokenPair(c, user)
  368. }
  369. // GetCurrentUser handles getting current authenticated user
  370. // GET /api/v1/auth/me
  371. func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
  372. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  373. if !ok {
  374. response.Unauthorized(c, "User not authenticated")
  375. return
  376. }
  377. user, err := h.userService.GetByID(c.Request.Context(), subject.UserID)
  378. if err != nil {
  379. response.ErrorFrom(c, err)
  380. return
  381. }
  382. identities, err := h.userService.GetProfileIdentitySummaries(c.Request.Context(), subject.UserID, user)
  383. if err != nil {
  384. response.ErrorFrom(c, err)
  385. return
  386. }
  387. type UserResponse struct {
  388. userProfileResponse
  389. RunMode string `json:"run_mode"`
  390. }
  391. runMode := config.RunModeStandard
  392. if h.cfg != nil {
  393. runMode = h.cfg.RunMode
  394. }
  395. response.Success(c, UserResponse{
  396. userProfileResponse: userProfileResponseFromService(user, identities),
  397. RunMode: runMode,
  398. })
  399. }
  400. // ValidatePromoCodeRequest 验证优惠码请求
  401. type ValidatePromoCodeRequest struct {
  402. Code string `json:"code" binding:"required"`
  403. }
  404. // ValidatePromoCodeResponse 验证优惠码响应
  405. type ValidatePromoCodeResponse struct {
  406. Valid bool `json:"valid"`
  407. BonusAmount float64 `json:"bonus_amount,omitempty"`
  408. ErrorCode string `json:"error_code,omitempty"`
  409. Message string `json:"message,omitempty"`
  410. }
  411. // ValidatePromoCode 验证优惠码(公开接口,注册前调用)
  412. // POST /api/v1/auth/validate-promo-code
  413. func (h *AuthHandler) ValidatePromoCode(c *gin.Context) {
  414. // 检查优惠码功能是否启用
  415. if h.settingSvc != nil && !h.settingSvc.IsPromoCodeEnabled(c.Request.Context()) {
  416. response.Success(c, ValidatePromoCodeResponse{
  417. Valid: false,
  418. ErrorCode: "PROMO_CODE_DISABLED",
  419. })
  420. return
  421. }
  422. var req ValidatePromoCodeRequest
  423. if err := c.ShouldBindJSON(&req); err != nil {
  424. response.BadRequest(c, "Invalid request: "+err.Error())
  425. return
  426. }
  427. promoCode, err := h.promoService.ValidatePromoCode(c.Request.Context(), req.Code)
  428. if err != nil {
  429. // 根据错误类型返回对应的错误码
  430. errorCode := "PROMO_CODE_INVALID"
  431. switch err {
  432. case service.ErrPromoCodeNotFound:
  433. errorCode = "PROMO_CODE_NOT_FOUND"
  434. case service.ErrPromoCodeExpired:
  435. errorCode = "PROMO_CODE_EXPIRED"
  436. case service.ErrPromoCodeDisabled:
  437. errorCode = "PROMO_CODE_DISABLED"
  438. case service.ErrPromoCodeMaxUsed:
  439. errorCode = "PROMO_CODE_MAX_USED"
  440. case service.ErrPromoCodeAlreadyUsed:
  441. errorCode = "PROMO_CODE_ALREADY_USED"
  442. }
  443. response.Success(c, ValidatePromoCodeResponse{
  444. Valid: false,
  445. ErrorCode: errorCode,
  446. })
  447. return
  448. }
  449. if promoCode == nil {
  450. response.Success(c, ValidatePromoCodeResponse{
  451. Valid: false,
  452. ErrorCode: "PROMO_CODE_INVALID",
  453. })
  454. return
  455. }
  456. response.Success(c, ValidatePromoCodeResponse{
  457. Valid: true,
  458. BonusAmount: promoCode.BonusAmount,
  459. })
  460. }
  461. // ValidateInvitationCodeRequest 验证邀请码请求
  462. type ValidateInvitationCodeRequest struct {
  463. Code string `json:"code" binding:"required"`
  464. }
  465. // ValidateInvitationCodeResponse 验证邀请码响应
  466. type ValidateInvitationCodeResponse struct {
  467. Valid bool `json:"valid"`
  468. ErrorCode string `json:"error_code,omitempty"`
  469. }
  470. // ValidateInvitationCode 验证邀请码(公开接口,注册前调用)
  471. // POST /api/v1/auth/validate-invitation-code
  472. func (h *AuthHandler) ValidateInvitationCode(c *gin.Context) {
  473. // 检查邀请码功能是否启用
  474. if h.settingSvc == nil || !h.settingSvc.IsInvitationCodeEnabled(c.Request.Context()) {
  475. response.Success(c, ValidateInvitationCodeResponse{
  476. Valid: false,
  477. ErrorCode: "INVITATION_CODE_DISABLED",
  478. })
  479. return
  480. }
  481. var req ValidateInvitationCodeRequest
  482. if err := c.ShouldBindJSON(&req); err != nil {
  483. response.BadRequest(c, "Invalid request: "+err.Error())
  484. return
  485. }
  486. // 验证邀请码
  487. redeemCode, err := h.redeemService.GetByCode(c.Request.Context(), req.Code)
  488. if err != nil {
  489. response.Success(c, ValidateInvitationCodeResponse{
  490. Valid: false,
  491. ErrorCode: "INVITATION_CODE_NOT_FOUND",
  492. })
  493. return
  494. }
  495. // 检查类型和状态
  496. if redeemCode.Type != service.RedeemTypeInvitation {
  497. response.Success(c, ValidateInvitationCodeResponse{
  498. Valid: false,
  499. ErrorCode: "INVITATION_CODE_INVALID",
  500. })
  501. return
  502. }
  503. if redeemCode.Status != service.StatusUnused {
  504. response.Success(c, ValidateInvitationCodeResponse{
  505. Valid: false,
  506. ErrorCode: "INVITATION_CODE_USED",
  507. })
  508. return
  509. }
  510. response.Success(c, ValidateInvitationCodeResponse{
  511. Valid: true,
  512. })
  513. }
  514. // ForgotPasswordRequest 忘记密码请求
  515. type ForgotPasswordRequest struct {
  516. Email string `json:"email" binding:"required,email"`
  517. TurnstileToken string `json:"turnstile_token"`
  518. TencentCaptchaTicket string `json:"tencent_captcha_ticket"`
  519. TencentCaptchaRandstr string `json:"tencent_captcha_randstr"`
  520. }
  521. // ForgotPasswordResponse 忘记密码响应
  522. type ForgotPasswordResponse struct {
  523. Message string `json:"message"`
  524. }
  525. // ForgotPassword 请求密码重置
  526. // POST /api/v1/auth/forgot-password
  527. func (h *AuthHandler) ForgotPassword(c *gin.Context) {
  528. var req ForgotPasswordRequest
  529. if err := c.ShouldBindJSON(&req); err != nil {
  530. response.BadRequest(c, "Invalid request: "+err.Error())
  531. return
  532. }
  533. proof := captchaProof(req.TurnstileToken, req.TencentCaptchaTicket, req.TencentCaptchaRandstr)
  534. if err := h.authService.VerifyCaptcha(c.Request.Context(), proof, ip.GetClientIP(c)); err != nil {
  535. response.ErrorFrom(c, err)
  536. return
  537. }
  538. frontendBaseURL := strings.TrimSpace(h.settingSvc.GetFrontendURL(c.Request.Context()))
  539. if frontendBaseURL == "" {
  540. slog.Error("frontend_url not configured in settings or config; cannot build password reset link")
  541. response.InternalError(c, "Password reset is not configured")
  542. return
  543. }
  544. // Request password reset (async)
  545. // Note: This returns success even if email doesn't exist (to prevent enumeration)
  546. if err := h.authService.RequestPasswordResetAsync(c.Request.Context(), req.Email, frontendBaseURL, c.GetHeader("Accept-Language")); err != nil {
  547. response.ErrorFrom(c, err)
  548. return
  549. }
  550. response.Success(c, ForgotPasswordResponse{
  551. Message: "If your email is registered, you will receive a password reset link shortly.",
  552. })
  553. }
  554. // ResetPasswordRequest 重置密码请求
  555. type ResetPasswordRequest struct {
  556. Email string `json:"email" binding:"required,email"`
  557. Token string `json:"token" binding:"required"`
  558. NewPassword string `json:"new_password" binding:"required,min=6"`
  559. }
  560. // ResetPasswordResponse 重置密码响应
  561. type ResetPasswordResponse struct {
  562. Message string `json:"message"`
  563. }
  564. // ResetPassword 重置密码
  565. // POST /api/v1/auth/reset-password
  566. func (h *AuthHandler) ResetPassword(c *gin.Context) {
  567. var req ResetPasswordRequest
  568. if err := c.ShouldBindJSON(&req); err != nil {
  569. response.BadRequest(c, "Invalid request: "+err.Error())
  570. return
  571. }
  572. // Reset password
  573. if err := h.authService.ResetPassword(c.Request.Context(), req.Email, req.Token, req.NewPassword); err != nil {
  574. response.ErrorFrom(c, err)
  575. return
  576. }
  577. response.Success(c, ResetPasswordResponse{
  578. Message: "Your password has been reset successfully. You can now log in with your new password.",
  579. })
  580. }
  581. // ==================== Token Refresh Endpoints ====================
  582. // RefreshTokenRequest 刷新Token请求
  583. type RefreshTokenRequest struct {
  584. RefreshToken string `json:"refresh_token" binding:"required"`
  585. }
  586. // RefreshTokenResponse 刷新Token响应
  587. type RefreshTokenResponse struct {
  588. AccessToken string `json:"access_token"`
  589. RefreshToken string `json:"refresh_token"`
  590. ExpiresIn int `json:"expires_in"` // Access Token有效期(秒)
  591. TokenType string `json:"token_type"`
  592. }
  593. // RefreshToken 刷新Token
  594. // POST /api/v1/auth/refresh
  595. func (h *AuthHandler) RefreshToken(c *gin.Context) {
  596. var req RefreshTokenRequest
  597. if err := c.ShouldBindJSON(&req); err != nil {
  598. response.BadRequest(c, "Invalid request: "+err.Error())
  599. return
  600. }
  601. result, err := h.authService.RefreshTokenPair(c.Request.Context(), req.RefreshToken)
  602. if err != nil {
  603. response.ErrorFrom(c, err)
  604. return
  605. }
  606. // Backend mode: block non-admin token refresh
  607. if h.settingSvc.IsBackendModeEnabled(c.Request.Context()) && !service.IsAdminRole(result.UserRole) {
  608. response.Forbidden(c, "Backend mode is active. Only admin login is allowed.")
  609. return
  610. }
  611. response.Success(c, RefreshTokenResponse{
  612. AccessToken: result.AccessToken,
  613. RefreshToken: result.RefreshToken,
  614. ExpiresIn: result.ExpiresIn,
  615. TokenType: "Bearer",
  616. })
  617. }
  618. // LogoutRequest 登出请求
  619. type LogoutRequest struct {
  620. RefreshToken string `json:"refresh_token,omitempty"` // 可选:撤销指定的Refresh Token
  621. }
  622. // LogoutResponse 登出响应
  623. type LogoutResponse struct {
  624. Message string `json:"message"`
  625. }
  626. // Logout 用户登出
  627. // POST /api/v1/auth/logout
  628. func (h *AuthHandler) Logout(c *gin.Context) {
  629. var req LogoutRequest
  630. // 允许空请求体(向后兼容)
  631. _ = c.ShouldBindJSON(&req)
  632. // 如果提供了Refresh Token,撤销它
  633. if req.RefreshToken != "" {
  634. if err := h.authService.RevokeRefreshToken(c.Request.Context(), req.RefreshToken); err != nil {
  635. slog.Debug("failed to revoke refresh token", "error", err)
  636. // 不影响登出流程
  637. }
  638. }
  639. h.consumePendingOAuthSessionOnLogout(c)
  640. clearOAuthLogoutCookies(c)
  641. response.Success(c, LogoutResponse{
  642. Message: "Logged out successfully",
  643. })
  644. }
  645. // RevokeAllSessionsResponse 撤销所有会话响应
  646. type RevokeAllSessionsResponse struct {
  647. Message string `json:"message"`
  648. }
  649. // RevokeAllSessions 撤销当前用户的所有会话
  650. // POST /api/v1/auth/revoke-all-sessions
  651. func (h *AuthHandler) RevokeAllSessions(c *gin.Context) {
  652. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  653. if !ok {
  654. response.Unauthorized(c, "User not authenticated")
  655. return
  656. }
  657. if err := h.authService.RevokeAllUserTokens(c.Request.Context(), subject.UserID); err != nil {
  658. slog.Error("failed to revoke all sessions", "user_id", subject.UserID, "error", err)
  659. response.InternalError(c, "Failed to revoke sessions")
  660. return
  661. }
  662. response.Success(c, RevokeAllSessionsResponse{
  663. Message: "All sessions have been revoked. Please log in again.",
  664. })
  665. }