totp_handler.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. package handler
  2. import (
  3. "github.com/gin-gonic/gin"
  4. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  5. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  6. "github.com/Wei-Shaw/sub2api/internal/service"
  7. )
  8. // TotpHandler handles TOTP-related requests
  9. type TotpHandler struct {
  10. totpService *service.TotpService
  11. }
  12. // NewTotpHandler creates a new TotpHandler
  13. func NewTotpHandler(totpService *service.TotpService) *TotpHandler {
  14. return &TotpHandler{
  15. totpService: totpService,
  16. }
  17. }
  18. // TotpStatusResponse represents the TOTP status response
  19. type TotpStatusResponse struct {
  20. Enabled bool `json:"enabled"`
  21. EnabledAt *int64 `json:"enabled_at,omitempty"` // Unix timestamp
  22. FeatureEnabled bool `json:"feature_enabled"`
  23. }
  24. // GetStatus returns the TOTP status for the current user
  25. // GET /api/v1/user/totp/status
  26. func (h *TotpHandler) GetStatus(c *gin.Context) {
  27. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  28. if !ok {
  29. response.Unauthorized(c, "User not authenticated")
  30. return
  31. }
  32. status, err := h.totpService.GetStatus(c.Request.Context(), subject.UserID)
  33. if err != nil {
  34. response.ErrorFrom(c, err)
  35. return
  36. }
  37. resp := TotpStatusResponse{
  38. Enabled: status.Enabled,
  39. FeatureEnabled: status.FeatureEnabled,
  40. }
  41. if status.EnabledAt != nil {
  42. ts := status.EnabledAt.Unix()
  43. resp.EnabledAt = &ts
  44. }
  45. response.Success(c, resp)
  46. }
  47. // TotpSetupRequest represents the request to initiate TOTP setup
  48. type TotpSetupRequest struct {
  49. EmailCode string `json:"email_code"`
  50. Password string `json:"password"`
  51. }
  52. // TotpSetupResponse represents the TOTP setup response
  53. type TotpSetupResponse struct {
  54. Secret string `json:"secret"`
  55. QRCodeURL string `json:"qr_code_url"`
  56. SetupToken string `json:"setup_token"`
  57. Countdown int `json:"countdown"`
  58. }
  59. // InitiateSetup starts the TOTP setup process
  60. // POST /api/v1/user/totp/setup
  61. func (h *TotpHandler) InitiateSetup(c *gin.Context) {
  62. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  63. if !ok {
  64. response.Unauthorized(c, "User not authenticated")
  65. return
  66. }
  67. var req TotpSetupRequest
  68. if err := c.ShouldBindJSON(&req); err != nil {
  69. // Allow empty body (optional params)
  70. req = TotpSetupRequest{}
  71. }
  72. result, err := h.totpService.InitiateSetup(c.Request.Context(), subject.UserID, req.EmailCode, req.Password)
  73. if err != nil {
  74. response.ErrorFrom(c, err)
  75. return
  76. }
  77. response.Success(c, TotpSetupResponse{
  78. Secret: result.Secret,
  79. QRCodeURL: result.QRCodeURL,
  80. SetupToken: result.SetupToken,
  81. Countdown: result.Countdown,
  82. })
  83. }
  84. // TotpEnableRequest represents the request to enable TOTP
  85. type TotpEnableRequest struct {
  86. TotpCode string `json:"totp_code" binding:"required,len=6"`
  87. SetupToken string `json:"setup_token" binding:"required"`
  88. }
  89. // Enable completes the TOTP setup
  90. // POST /api/v1/user/totp/enable
  91. func (h *TotpHandler) Enable(c *gin.Context) {
  92. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  93. if !ok {
  94. response.Unauthorized(c, "User not authenticated")
  95. return
  96. }
  97. var req TotpEnableRequest
  98. if err := c.ShouldBindJSON(&req); err != nil {
  99. response.BadRequest(c, "Invalid request: "+err.Error())
  100. return
  101. }
  102. if err := h.totpService.CompleteSetup(c.Request.Context(), subject.UserID, req.TotpCode, req.SetupToken); err != nil {
  103. response.ErrorFrom(c, err)
  104. return
  105. }
  106. response.Success(c, gin.H{"success": true})
  107. }
  108. // TotpDisableRequest represents the request to disable TOTP
  109. type TotpDisableRequest struct {
  110. EmailCode string `json:"email_code"`
  111. Password string `json:"password"`
  112. }
  113. // Disable disables TOTP for the current user
  114. // POST /api/v1/user/totp/disable
  115. func (h *TotpHandler) Disable(c *gin.Context) {
  116. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  117. if !ok {
  118. response.Unauthorized(c, "User not authenticated")
  119. return
  120. }
  121. var req TotpDisableRequest
  122. if err := c.ShouldBindJSON(&req); err != nil {
  123. response.BadRequest(c, "Invalid request: "+err.Error())
  124. return
  125. }
  126. if err := h.totpService.Disable(c.Request.Context(), subject.UserID, req.EmailCode, req.Password); err != nil {
  127. response.ErrorFrom(c, err)
  128. return
  129. }
  130. response.Success(c, gin.H{"success": true})
  131. }
  132. // GetVerificationMethod returns the verification method for TOTP operations
  133. // GET /api/v1/user/totp/verification-method
  134. func (h *TotpHandler) GetVerificationMethod(c *gin.Context) {
  135. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  136. if !ok {
  137. response.Unauthorized(c, "User not authenticated")
  138. return
  139. }
  140. method, err := h.totpService.GetVerificationMethod(c.Request.Context(), subject.UserID)
  141. if err != nil {
  142. response.ErrorFrom(c, err)
  143. return
  144. }
  145. response.Success(c, method)
  146. }
  147. // SendVerifyCode sends an email verification code for TOTP operations
  148. // POST /api/v1/user/totp/send-code
  149. func (h *TotpHandler) SendVerifyCode(c *gin.Context) {
  150. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  151. if !ok {
  152. response.Unauthorized(c, "User not authenticated")
  153. return
  154. }
  155. if err := h.totpService.SendVerifyCode(c.Request.Context(), subject.UserID, c.GetHeader("Accept-Language")); err != nil {
  156. response.ErrorFrom(c, err)
  157. return
  158. }
  159. response.Success(c, gin.H{"success": true})
  160. }
  161. // TotpStepUpRequest represents the request to verify a step-up TOTP code
  162. type TotpStepUpRequest struct {
  163. Code string `json:"code" binding:"required"`
  164. }
  165. // TotpStepUpResponse represents the step-up verification response
  166. type TotpStepUpResponse struct {
  167. Verified bool `json:"verified"`
  168. ExpiresIn int64 `json:"expires_in"` // 授权剩余有效期(秒)
  169. }
  170. // StepUp 敏感操作二次验证:校验 TOTP 码并为当前会话授予一段时间的 step-up 权限。
  171. // POST /api/v1/user/totp/step-up
  172. func (h *TotpHandler) StepUp(c *gin.Context) {
  173. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  174. if !ok {
  175. response.Unauthorized(c, "User not authenticated")
  176. return
  177. }
  178. var req TotpStepUpRequest
  179. if err := c.ShouldBindJSON(&req); err != nil {
  180. response.BadRequest(c, "TOTP code is required")
  181. return
  182. }
  183. sessionKey := middleware2.StepUpSessionKey(c, subject.UserID)
  184. ttl, err := h.totpService.VerifyStepUp(c.Request.Context(), subject.UserID, sessionKey, req.Code)
  185. if err != nil {
  186. response.ErrorFrom(c, err)
  187. return
  188. }
  189. response.Success(c, TotpStepUpResponse{
  190. Verified: true,
  191. ExpiresIn: int64(ttl.Seconds()),
  192. })
  193. }