package middleware import ( "context" "net/http" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // CaptchaVerifier defines the interface for CAPTCHA verification. type CaptchaVerifier interface { Verify(ctx context.Context, token, remoteIP string) error IsEnabled() bool } // RequireCaptcha creates a middleware that verifies CAPTCHA tokens. // F027/F029: Applied to registration and login endpoints. // Token is read from the "captcha_token" field in JSON body or "X-Captcha-Token" header. func RequireCaptcha(verifier CaptchaVerifier, logger *zap.Logger) gin.HandlerFunc { return func(c *gin.Context) { if verifier == nil || !verifier.IsEnabled() { c.Next() return } // Try header first, then query param token := c.GetHeader("X-Captcha-Token") if token == "" { token = c.Query("captcha_token") } // For POST requests, try to read from form or JSON body if token == "" { token = c.PostForm("captcha_token") } if token == "" { logger.Warn("CAPTCHA token missing", zap.String("path", c.Request.URL.Path), zap.String("ip", c.ClientIP()), ) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ "error": gin.H{ "code": "captcha_required", "message": "CAPTCHA verification is required", }, }) return } if err := verifier.Verify(c.Request.Context(), token, c.ClientIP()); err != nil { logger.Warn("CAPTCHA verification failed", zap.String("path", c.Request.URL.Path), zap.String("ip", c.ClientIP()), zap.Error(err), ) c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "error": gin.H{ "code": "captcha_invalid", "message": "CAPTCHA verification failed", }, }) return } c.Next() } }