veza/veza-backend-api/internal/handlers/two_factor_handler.go
senke 8592b3c76b [BE-API-001] api: Implement 2FA endpoints (setup, verify, disable)
- Created TwoFactorHandler with SetupTwoFactor, VerifyTwoFactor, DisableTwoFactor, GetTwoFactorStatus
- Added routes: POST /auth/2fa/setup, POST /auth/2fa/verify, POST /auth/2fa/disable, GET /auth/2fa/status
- Updated LoginResponse DTO to include requires_2fa flag
- Updated Login handler to check 2FA status and return requires_2fa flag when enabled
- Reused existing TwoFactorService (already had QR generation and TOTP verification)
- Added VerifyTOTPCode helper method to TwoFactorService
- All endpoints properly authenticated with RequireAuth middleware

Phase: PHASE-1
Priority: P0
Progress: 4/267 (1.5%)
2025-12-23 01:40:28 +01:00

251 lines
8.8 KiB
Go

package handlers
import (
"net/http"
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
apperrors "veza-backend-api/internal/errors"
"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})
}