package v1 import ( "net/http" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/auth" "github.com/gochat/gochat/pkg/response" ) // Reference: P2E §1.5 — MFA HTTP handlers // Maps to Chatwoot enterprise TwoFactorAuthController: // - enable → POST /api/v1/auth/mfa/enable (generates secret + QR URI) // - verify → POST /api/v1/auth/mfa/verify (validates TOTP code, enables MFA) // - disable → POST /api/v1/auth/mfa/disable (disables MFA after code verification) // MFAHandler handles MFA (TOTP) HTTP endpoints. type MFAHandler struct { mfaService *auth.MFAService } // NewMFAHandler creates a MFA handler with service dependency. func NewMFAHandler(mfaService *auth.MFAService) *MFAHandler { return &MFAHandler{ mfaService: mfaService, } } // --- Request/Response structs --- // EnableMFARequest is the JSON body for MFA enablement initiation. type EnableMFARequest struct { // No body required — user_id comes from auth context } // EnableMFAResponse is the JSON response for MFA enablement initiation. type EnableMFAResponse struct { TOTPSecret string `json:"totp_secret"` // base32 secret for manual entry QRURI string `json:"qr_uri"` // otpauth:// URI for QR code generation Message string `json:"message"` } // VerifyMFARequest is the JSON body for MFA TOTP verification. type VerifyMFARequest struct { TOTPSecret string `json:"totp_secret" binding:"required"` // secret from enable step TOTPCode string `json:"totp_code" binding:"required"` // 6-digit code from authenticator app } // DisableMFARequest is the JSON body for MFA disablement. type DisableMFARequest struct { TOTPCode string `json:"totp_code" binding:"required"` // current TOTP code for verification } // --- Handlers --- // EnableMFA initiates MFA setup: generates a TOTP secret and QR URI. // POST /api/v1/auth/mfa/enable // Requires authentication — uses user_id from JWT context. // The user must verify a TOTP code before MFA is actually activated. func (h *MFAHandler) EnableMFA(c *gin.Context) { userID := c.GetUint("user_id") if userID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "Authentication required") return } // Check if MFA is already enabled enabled, err := h.mfaService.IsMFAEnabled(userID) if err != nil { response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, err.Error()) return } if enabled { response.AbortWithStatusError(c, http.StatusConflict, response.ErrConflict, "MFA is already enabled for this user") return } // Generate new TOTP secret + QR URI secret, qrURI, err := h.mfaService.GenerateTOTPSecret(userID) if err != nil { response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, err.Error()) return } response.OK(c, EnableMFAResponse{ TOTPSecret: secret, QRURI: qrURI, Message: "Scan QR code with your authenticator app, then verify with a TOTP code", }) } // VerifyMFA completes MFA setup: verifies TOTP code and enables MFA on the user. // POST /api/v1/auth/mfa/verify // Requires authentication — uses user_id from JWT context. // This is the second step: user provides secret + code from authenticator app. func (h *MFAHandler) VerifyMFA(c *gin.Context) { userID := c.GetUint("user_id") if userID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "Authentication required") return } var req VerifyMFARequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } // Validate the TOTP code against the provided secret cfg := auth.DefaultTOTPConfig() if !auth.ValidateTOTPCode(req.TOTPSecret, req.TOTPCode, cfg) { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "Invalid TOTP code, please try again") return } // Enable TOTP on the user (stores secret in DB) if err := h.mfaService.EnableTOTP(userID, req.TOTPSecret); err != nil { response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, err.Error()) return } response.OK(c, gin.H{ "message": "MFA enabled successfully", "mfa_enabled": true, }) } // DisableMFA disables MFA after verifying the current TOTP code. // POST /api/v1/auth/mfa/disable // Requires authentication — uses user_id from JWT context. func (h *MFAHandler) DisableMFA(c *gin.Context) { userID := c.GetUint("user_id") if userID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "Authentication required") return } var req DisableMFARequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } // Disable TOTP (requires valid current code for security) if err := h.mfaService.DisableTOTP(userID, req.TOTPCode); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error()) return } response.OK(c, gin.H{ "message": "MFA disabled successfully", "mfa_enabled": false, }) } // MFAStatus returns the current MFA status for the authenticated user. // GET /api/v1/auth/mfa/status func (h *MFAHandler) MFAStatus(c *gin.Context) { userID := c.GetUint("user_id") if userID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "Authentication required") return } enabled, err := h.mfaService.IsMFAEnabled(userID) if err != nil { response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, err.Error()) return } response.OK(c, gin.H{ "mfa_enabled": enabled, }) } // RegisterMFARoutes sets up MFA routes on a Gin router group. // These routes require authentication (AuthRequired middleware). func RegisterMFARoutes(rg *gin.RouterGroup, handler *MFAHandler) { mfaGroup := rg.Group("/auth/mfa") { mfaGroup.POST("/enable", handler.EnableMFA) mfaGroup.POST("/verify", handler.VerifyMFA) mfaGroup.POST("/disable", handler.DisableMFA) mfaGroup.GET("/status", handler.MFAStatus) mfaGroup.POST("/backup_codes", handler.BackupCodes) } } // BackupCodes generates one-time MFA backup codes. // POST /api/v1/profile/mfa/backup_codes or /api/v1/auth/mfa/backup_codes // Reference: Chatwoot MfaController#backup_codes func (h *MFAHandler) BackupCodes(c *gin.Context) { userID := getUserID(c) if userID == 0 { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"}) return } codes, err := h.mfaService.GenerateBackupCodes(userID) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"backup_codes": codes}) }