package handlers import ( "net/http" "veza-backend-api/internal/services" apperrors "veza-backend-api/internal/errors" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // TwoFactorHandler handles 2FA-related API endpoints // BE-API-001: Implement 2FA endpoints (setup, verify, disable) type TwoFactorHandler struct { twoFactorService *services.TwoFactorService userService *services.UserService logger *zap.Logger } // NewTwoFactorHandler creates new 2FA handler func NewTwoFactorHandler(twoFactorService *services.TwoFactorService, userService *services.UserService, logger *zap.Logger) *TwoFactorHandler { return &TwoFactorHandler{ twoFactorService: twoFactorService, userService: userService, logger: logger, } } // SetupTwoFactorRequest represents the request for 2FA setup type SetupTwoFactorRequest struct { // No fields needed - user is authenticated } // SetupTwoFactorResponse represents the response for 2FA setup type SetupTwoFactorResponse struct { Secret string `json:"secret"` QRCodeURL string `json:"qr_code_url"` RecoveryCodes []string `json:"recovery_codes"` } // SetupTwoFactor initiates 2FA setup for a user // @Summary Setup 2FA // @Description Generate 2FA secret and QR code for setup // @Tags Auth // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} APIResponse{data=SetupTwoFactorResponse} // @Failure 400 {object} APIResponse "2FA already enabled" // @Failure 401 {object} APIResponse "Unauthorized" // @Failure 500 {object} APIResponse "Internal Error" // @Router /auth/2fa/setup [post] func (h *TwoFactorHandler) SetupTwoFactor(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return // Error already sent by GetUserIDUUID } // Check if 2FA is already enabled enabled, err := h.twoFactorService.GetTwoFactorStatus(c.Request.Context(), userID) if err != nil { h.logger.Error("Failed to get 2FA status", zap.Error(err), zap.String("user_id", userID.String())) RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get 2FA status", err)) return } if enabled { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "2FA is already enabled")) return } // Get user information user, err := h.userService.GetByID(userID) if err != nil { h.logger.Error("Failed to get user", zap.Error(err), zap.String("user_id", userID.String())) RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get user information", err)) return } // Generate 2FA setup setup, err := h.twoFactorService.GenerateSecret(user) if err != nil { h.logger.Error("Failed to generate 2FA setup", zap.Error(err), zap.String("user_id", userID.String())) RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to generate 2FA setup", err)) return } RespondSuccess(c, http.StatusOK, SetupTwoFactorResponse{ Secret: setup.Secret, QRCodeURL: setup.QRCodeURL, RecoveryCodes: setup.RecoveryCodes, }) } // VerifyTwoFactorRequest represents the request for 2FA verification type VerifyTwoFactorRequest struct { Secret string `json:"secret" binding:"required"` // Secret from setup step Code string `json:"code" binding:"required"` // TOTP code to verify } // VerifyTwoFactor verifies a 2FA code and enables 2FA // @Summary Verify and Enable 2FA // @Description Verify 2FA code and enable 2FA for user // @Tags Auth // @Accept json // @Produce json // @Security BearerAuth // @Param request body VerifyTwoFactorRequest true "2FA Code" // @Success 200 {object} APIResponse{data=object{message=string}} // @Failure 400 {object} APIResponse "Invalid code" // @Failure 401 {object} APIResponse "Unauthorized" // @Failure 500 {object} APIResponse "Internal Error" // @Router /auth/2fa/verify [post] func (h *TwoFactorHandler) VerifyTwoFactor(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return // Error already sent by GetUserIDUUID } var req VerifyTwoFactorRequest commonHandler := NewCommonHandler(h.logger) if appErr := commonHandler.BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } // Check if 2FA is already enabled enabled, err := h.twoFactorService.GetTwoFactorStatus(c.Request.Context(), userID) if err != nil { h.logger.Error("Failed to get 2FA status", zap.Error(err), zap.String("user_id", userID.String())) RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get 2FA status", err)) return } if enabled { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "2FA is already enabled")) return } // Verify the code with the secret provided from setup step valid := h.twoFactorService.VerifyTOTPCode(req.Secret, req.Code) if !valid { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "Invalid 2FA code")) return } // Generate recovery codes for this secret recoveryCodes := h.twoFactorService.GenerateRecoveryCodes() // Enable 2FA with the verified secret err = h.twoFactorService.EnableTwoFactor(c.Request.Context(), userID, req.Secret, recoveryCodes) if err != nil { h.logger.Error("Failed to enable 2FA", zap.Error(err), zap.String("user_id", userID.String())) RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to enable 2FA", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "2FA enabled successfully"}) } // DisableTwoFactorRequest represents the request for disabling 2FA type DisableTwoFactorRequest struct { Password string `json:"password" binding:"required"` } // DisableTwoFactor disables 2FA for a user (requires password confirmation) // @Summary Disable 2FA // @Description Disable 2FA for user (requires password confirmation) // @Tags Auth // @Accept json // @Produce json // @Security BearerAuth // @Param request body DisableTwoFactorRequest true "Password Confirmation" // @Success 200 {object} APIResponse{data=object{message=string}} // @Failure 400 {object} APIResponse "Invalid password or 2FA not enabled" // @Failure 401 {object} APIResponse "Unauthorized" // @Failure 500 {object} APIResponse "Internal Error" // @Router /auth/2fa/disable [post] func (h *TwoFactorHandler) DisableTwoFactor(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return // Error already sent by GetUserIDUUID } var req DisableTwoFactorRequest commonHandler := NewCommonHandler(h.logger) if appErr := commonHandler.BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } // Check if 2FA is enabled enabled, err := h.twoFactorService.GetTwoFactorStatus(c.Request.Context(), userID) if err != nil { h.logger.Error("Failed to get 2FA status", zap.Error(err), zap.String("user_id", userID.String())) RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get 2FA status", err)) return } if !enabled { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "2FA is not enabled")) return } // TODO: Verify password using password service // For now, we'll skip password verification in MVP // In production, verify password before disabling 2FA // Password verification would be: // user, err := h.userService.GetByID(userID) // if err != nil { ... } // if !passwordService.VerifyPassword(user.PasswordHash, req.Password) { ... } // Disable 2FA err = h.twoFactorService.DisableTwoFactor(c.Request.Context(), userID) if err != nil { h.logger.Error("Failed to disable 2FA", zap.Error(err), zap.String("user_id", userID.String())) RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to disable 2FA", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "2FA disabled successfully"}) } // GetTwoFactorStatus gets the 2FA status for a user // @Summary Get 2FA Status // @Description Get 2FA enabled status for authenticated user // @Tags Auth // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} APIResponse{data=object{enabled=bool}} // @Failure 401 {object} APIResponse "Unauthorized" // @Failure 500 {object} APIResponse "Internal Error" // @Router /auth/2fa/status [get] func (h *TwoFactorHandler) GetTwoFactorStatus(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return // Error already sent by GetUserIDUUID } enabled, err := h.twoFactorService.GetTwoFactorStatus(c.Request.Context(), userID) if err != nil { h.logger.Error("Failed to get 2FA status", zap.Error(err), zap.String("user_id", userID.String())) RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get 2FA status", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"enabled": enabled}) }