| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- package handler
- import (
- "github.com/gin-gonic/gin"
- "github.com/Wei-Shaw/sub2api/internal/pkg/response"
- middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
- "github.com/Wei-Shaw/sub2api/internal/service"
- )
- // TotpHandler handles TOTP-related requests
- type TotpHandler struct {
- totpService *service.TotpService
- }
- // NewTotpHandler creates a new TotpHandler
- func NewTotpHandler(totpService *service.TotpService) *TotpHandler {
- return &TotpHandler{
- totpService: totpService,
- }
- }
- // TotpStatusResponse represents the TOTP status response
- type TotpStatusResponse struct {
- Enabled bool `json:"enabled"`
- EnabledAt *int64 `json:"enabled_at,omitempty"` // Unix timestamp
- FeatureEnabled bool `json:"feature_enabled"`
- }
- // GetStatus returns the TOTP status for the current user
- // GET /api/v1/user/totp/status
- func (h *TotpHandler) GetStatus(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- status, err := h.totpService.GetStatus(c.Request.Context(), subject.UserID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- resp := TotpStatusResponse{
- Enabled: status.Enabled,
- FeatureEnabled: status.FeatureEnabled,
- }
- if status.EnabledAt != nil {
- ts := status.EnabledAt.Unix()
- resp.EnabledAt = &ts
- }
- response.Success(c, resp)
- }
- // TotpSetupRequest represents the request to initiate TOTP setup
- type TotpSetupRequest struct {
- EmailCode string `json:"email_code"`
- Password string `json:"password"`
- }
- // TotpSetupResponse represents the TOTP setup response
- type TotpSetupResponse struct {
- Secret string `json:"secret"`
- QRCodeURL string `json:"qr_code_url"`
- SetupToken string `json:"setup_token"`
- Countdown int `json:"countdown"`
- }
- // InitiateSetup starts the TOTP setup process
- // POST /api/v1/user/totp/setup
- func (h *TotpHandler) InitiateSetup(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- var req TotpSetupRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- // Allow empty body (optional params)
- req = TotpSetupRequest{}
- }
- result, err := h.totpService.InitiateSetup(c.Request.Context(), subject.UserID, req.EmailCode, req.Password)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, TotpSetupResponse{
- Secret: result.Secret,
- QRCodeURL: result.QRCodeURL,
- SetupToken: result.SetupToken,
- Countdown: result.Countdown,
- })
- }
- // TotpEnableRequest represents the request to enable TOTP
- type TotpEnableRequest struct {
- TotpCode string `json:"totp_code" binding:"required,len=6"`
- SetupToken string `json:"setup_token" binding:"required"`
- }
- // Enable completes the TOTP setup
- // POST /api/v1/user/totp/enable
- func (h *TotpHandler) Enable(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- var req TotpEnableRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- if err := h.totpService.CompleteSetup(c.Request.Context(), subject.UserID, req.TotpCode, req.SetupToken); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, gin.H{"success": true})
- }
- // TotpDisableRequest represents the request to disable TOTP
- type TotpDisableRequest struct {
- EmailCode string `json:"email_code"`
- Password string `json:"password"`
- }
- // Disable disables TOTP for the current user
- // POST /api/v1/user/totp/disable
- func (h *TotpHandler) Disable(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- var req TotpDisableRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- if err := h.totpService.Disable(c.Request.Context(), subject.UserID, req.EmailCode, req.Password); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, gin.H{"success": true})
- }
- // GetVerificationMethod returns the verification method for TOTP operations
- // GET /api/v1/user/totp/verification-method
- func (h *TotpHandler) GetVerificationMethod(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- method, err := h.totpService.GetVerificationMethod(c.Request.Context(), subject.UserID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, method)
- }
- // SendVerifyCode sends an email verification code for TOTP operations
- // POST /api/v1/user/totp/send-code
- func (h *TotpHandler) SendVerifyCode(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- if err := h.totpService.SendVerifyCode(c.Request.Context(), subject.UserID, c.GetHeader("Accept-Language")); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, gin.H{"success": true})
- }
- // TotpStepUpRequest represents the request to verify a step-up TOTP code
- type TotpStepUpRequest struct {
- Code string `json:"code" binding:"required"`
- }
- // TotpStepUpResponse represents the step-up verification response
- type TotpStepUpResponse struct {
- Verified bool `json:"verified"`
- ExpiresIn int64 `json:"expires_in"` // 授权剩余有效期(秒)
- }
- // StepUp 敏感操作二次验证:校验 TOTP 码并为当前会话授予一段时间的 step-up 权限。
- // POST /api/v1/user/totp/step-up
- func (h *TotpHandler) StepUp(c *gin.Context) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return
- }
- var req TotpStepUpRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "TOTP code is required")
- return
- }
- sessionKey := middleware2.StepUpSessionKey(c, subject.UserID)
- ttl, err := h.totpService.VerifyStepUp(c.Request.Context(), subject.UserID, sessionKey, req.Code)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, TotpStepUpResponse{
- Verified: true,
- ExpiresIn: int64(ttl.Seconds()),
- })
- }
|