added hcaptcha
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"tm/pkg/authorization"
|
"tm/pkg/authorization"
|
||||||
"tm/pkg/config"
|
"tm/pkg/config"
|
||||||
|
"tm/pkg/hcaptcha"
|
||||||
"tm/pkg/logger"
|
"tm/pkg/logger"
|
||||||
"tm/pkg/mongo"
|
"tm/pkg/mongo"
|
||||||
"tm/pkg/notification"
|
"tm/pkg/notification"
|
||||||
@@ -249,3 +250,21 @@ func InitOllamaService(conf config.OllamaConfig, log logger.Logger) ollama.SDK {
|
|||||||
|
|
||||||
return *ollamaSDK
|
return *ollamaSDK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitHCaptchaService initializes the hCaptcha verifier
|
||||||
|
func InitHCaptchaService(conf config.HCaptchaConfig, log logger.Logger) hcaptcha.Verifier {
|
||||||
|
hcaptchaConfig := &hcaptcha.Config{
|
||||||
|
SecretKey: conf.SecretKey,
|
||||||
|
VerifyURL: conf.VerifyURL,
|
||||||
|
Timeout: conf.Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
hcaptchaVerifier := hcaptcha.NewVerifier(hcaptchaConfig, log)
|
||||||
|
|
||||||
|
log.Info("hCaptcha verifier initialized successfully", map[string]interface{}{
|
||||||
|
"verify_url": conf.VerifyURL,
|
||||||
|
"timeout": conf.Timeout,
|
||||||
|
})
|
||||||
|
|
||||||
|
return hcaptchaVerifier
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type Config struct {
|
|||||||
Assets config.AssetsConfig
|
Assets config.AssetsConfig
|
||||||
Notification config.NotificationConfig
|
Notification config.NotificationConfig
|
||||||
Ollama config.OllamaConfig
|
Ollama config.OllamaConfig
|
||||||
|
HCaptcha config.HCaptchaConfig
|
||||||
Scraper ScraperConfig
|
Scraper ScraperConfig
|
||||||
UserAuth AuthConfig
|
UserAuth AuthConfig
|
||||||
CustomerAuth AuthConfig
|
CustomerAuth AuthConfig
|
||||||
|
|||||||
+5
-2
@@ -144,6 +144,9 @@ func main() {
|
|||||||
// Initialize notification service
|
// Initialize notification service
|
||||||
notificationSDK := bootstrap.InitNotificationService(conf.Notification, logger)
|
notificationSDK := bootstrap.InitNotificationService(conf.Notification, logger)
|
||||||
|
|
||||||
|
// Initialize hCaptcha verifier
|
||||||
|
hcaptchaVerifier := bootstrap.InitHCaptchaService(conf.HCaptcha, logger)
|
||||||
|
|
||||||
// Initialize authorization service
|
// Initialize authorization service
|
||||||
userAuthService := bootstrap.InitAuthorizationService(conf.UserAuth.JWT, redisClient, logger)
|
userAuthService := bootstrap.InitAuthorizationService(conf.UserAuth.JWT, redisClient, logger)
|
||||||
customerAuthService := bootstrap.InitAuthorizationService(conf.CustomerAuth.JWT, redisClient, logger)
|
customerAuthService := bootstrap.InitAuthorizationService(conf.CustomerAuth.JWT, redisClient, logger)
|
||||||
@@ -193,10 +196,10 @@ func main() {
|
|||||||
tenderHandler := tender.NewHandler(tenderService, logger)
|
tenderHandler := tender.NewHandler(tenderService, logger)
|
||||||
feedbackHandler := feedback.NewHandler(feedbackService, userHandler, customerHandler, logger)
|
feedbackHandler := feedback.NewHandler(feedbackService, userHandler, customerHandler, logger)
|
||||||
tenderApprovalHandler := tender_approval.NewHandler(tenderApprovalService, logger)
|
tenderApprovalHandler := tender_approval.NewHandler(tenderApprovalService, logger)
|
||||||
inquiryHandler := inquiry.NewHandler(inquiryService, logger)
|
inquiryHandler := inquiry.NewHandler(inquiryService, logger, hcaptchaVerifier)
|
||||||
flagHandler := assets.NewHandler(flagService, logger)
|
flagHandler := assets.NewHandler(flagService, logger)
|
||||||
notificationHandler := notification.NewHandler(notificationService)
|
notificationHandler := notification.NewHandler(notificationService)
|
||||||
contactHandler := contact.NewHandler(contactService)
|
contactHandler := contact.NewHandler(contactService, hcaptchaVerifier)
|
||||||
cmsHandler := cms.NewHandler(cmsService)
|
cmsHandler := cms.NewHandler(cmsService)
|
||||||
scraperHandler := scraper.NewHandler(scraperService)
|
scraperHandler := scraper.NewHandler(scraperService)
|
||||||
logger.Info("Handlers initialized successfully", map[string]interface{}{
|
logger.Info("Handlers initialized successfully", map[string]interface{}{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type CreateContactForm struct {
|
|||||||
Email string `json:"email" valid:"required,email" example:"john.doe@example.com"`
|
Email string `json:"email" valid:"required,email" example:"john.doe@example.com"`
|
||||||
Phone string `json:"phone" valid:"required,length(10|20)" example:"+1234567890"`
|
Phone string `json:"phone" valid:"required,length(10|20)" example:"+1234567890"`
|
||||||
Message string `json:"message" valid:"required,length(10|1000)" example:"I have a question about your services"`
|
Message string `json:"message" valid:"required,length(10|1000)" example:"I have a question about your services"`
|
||||||
|
HCaptchaToken string `json:"hcaptcha_token" valid:"required" example:"P0_eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateContactStatusForm represents the form for updating contact status (admin only)
|
// UpdateContactStatusForm represents the form for updating contact status (admin only)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package contact
|
package contact
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"tm/pkg/hcaptcha"
|
||||||
"tm/pkg/response"
|
"tm/pkg/response"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
@@ -9,12 +10,14 @@ import (
|
|||||||
// Handler handles HTTP requests for contact operations
|
// Handler handles HTTP requests for contact operations
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
service Service
|
service Service
|
||||||
|
hcaptchaVerifier hcaptcha.Verifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new contact handler
|
// NewHandler creates a new contact handler
|
||||||
func NewHandler(service Service) *Handler {
|
func NewHandler(service Service, hcaptchaVerifier hcaptcha.Verifier) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
service: service,
|
service: service,
|
||||||
|
hcaptchaVerifier: hcaptchaVerifier,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +38,17 @@ func (h *Handler) Create(c echo.Context) error {
|
|||||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify hCaptcha token
|
||||||
|
remoteIP := c.RealIP()
|
||||||
|
verificationResponse, err := h.hcaptchaVerifier.Verify(form.HCaptchaToken, remoteIP)
|
||||||
|
if err != nil {
|
||||||
|
return response.BadRequest(c, "Failed to verify hCaptcha token", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !verificationResponse.Success {
|
||||||
|
return response.BadRequest(c, "hCaptcha verification failed", "")
|
||||||
|
}
|
||||||
|
|
||||||
// Create contact
|
// Create contact
|
||||||
contact, err := h.service.Create(c.Request().Context(), form)
|
contact, err := h.service.Create(c.Request().Context(), form)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type CreateInquiryForm struct {
|
|||||||
CompanyName string `json:"company_name" valid:"required,length(2|100)" example:"Opplens"` // Company name
|
CompanyName string `json:"company_name" valid:"required,length(2|100)" example:"Opplens"` // Company name
|
||||||
WorkEmail string `json:"work_email" valid:"required,email" example:"info@opplens.com"` // Work email address
|
WorkEmail string `json:"work_email" valid:"required,email" example:"info@opplens.com"` // Work email address
|
||||||
PhoneNumber string `json:"phone_number" valid:"required,length(10|20)" example:"+1234567890"` // Phone number
|
PhoneNumber string `json:"phone_number" valid:"required,length(10|20)" example:"+1234567890"` // Phone number
|
||||||
|
HCaptchaToken string `json:"hcaptcha_token" valid:"required" example:"P0_eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateInquiryStatusForm represents the data required to update inquiry status
|
// UpdateInquiryStatusForm represents the data required to update inquiry status
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package inquiry
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"tm/pkg/hcaptcha"
|
||||||
"tm/pkg/logger"
|
"tm/pkg/logger"
|
||||||
"tm/pkg/response"
|
"tm/pkg/response"
|
||||||
|
|
||||||
@@ -13,13 +14,15 @@ import (
|
|||||||
type Handler struct {
|
type Handler struct {
|
||||||
service Service
|
service Service
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
|
hcaptchaVerifier hcaptcha.Verifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewInquiryHandler creates a new inquiry handler
|
// NewInquiryHandler creates a new inquiry handler
|
||||||
func NewHandler(service Service, logger logger.Logger) *Handler {
|
func NewHandler(service Service, logger logger.Logger, hcaptchaVerifier hcaptcha.Verifier) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
service: service,
|
service: service,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
hcaptchaVerifier: hcaptchaVerifier,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +50,17 @@ func (h *Handler) CreateInquiry(c echo.Context) error {
|
|||||||
return response.ValidationError(c, err.Error(), "")
|
return response.ValidationError(c, err.Error(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify hCaptcha token
|
||||||
|
remoteIP := c.RealIP()
|
||||||
|
verificationResponse, err := h.hcaptchaVerifier.Verify(form.HCaptchaToken, remoteIP)
|
||||||
|
if err != nil {
|
||||||
|
return response.BadRequest(c, "Failed to verify hCaptcha token", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !verificationResponse.Success {
|
||||||
|
return response.BadRequest(c, "hCaptcha verification failed", "")
|
||||||
|
}
|
||||||
|
|
||||||
// Call service
|
// Call service
|
||||||
inquiry, err := h.service.Create(c.Request().Context(), form)
|
inquiry, err := h.service.Create(c.Request().Context(), form)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -89,6 +89,12 @@ type OllamaConfig struct {
|
|||||||
EnableStreaming bool `env:"OLLAMA_ENABLE_STREAMING" default:"false"`
|
EnableStreaming bool `env:"OLLAMA_ENABLE_STREAMING" default:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HCaptchaConfig struct {
|
||||||
|
SecretKey string `env:"HCAPTCHA_SECRET_KEY"`
|
||||||
|
VerifyURL string `env:"HCAPTCHA_VERIFY_URL"`
|
||||||
|
Timeout time.Duration `env:"HCAPTCHA_TIMEOUT"`
|
||||||
|
}
|
||||||
|
|
||||||
// LoadConfig loads configuration with priority: OS environment variables > .env file
|
// LoadConfig loads configuration with priority: OS environment variables > .env file
|
||||||
// OS environment variables take precedence over .env file values
|
// OS environment variables take precedence over .env file values
|
||||||
func LoadConfig[T any](path string, config T) (T, error) {
|
func LoadConfig[T any](path string, config T) (T, error) {
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package hcaptcha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tm/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultVerifyURL is the default hCaptcha verification endpoint
|
||||||
|
DefaultVerifyURL = "https://hcaptcha.com/siteverify"
|
||||||
|
// DefaultTimeout is the default timeout for hCaptcha verification requests
|
||||||
|
DefaultTimeout = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verifier handles hCaptcha token verification
|
||||||
|
type Verifier interface {
|
||||||
|
Verify(token string, remoteIP string) (*VerificationResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config holds configuration for hCaptcha verification
|
||||||
|
type Config struct {
|
||||||
|
SecretKey string `env:"HCAPTCHA_SECRET_KEY"`
|
||||||
|
VerifyURL string `env:"HCAPTCHA_VERIFY_URL"`
|
||||||
|
Timeout time.Duration `env:"HCAPTCHA_TIMEOUT"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerificationResponse represents the response from hCaptcha verification API
|
||||||
|
type VerificationResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
ChallengeTS string `json:"challenge_ts,omitempty"`
|
||||||
|
Hostname string `json:"hostname,omitempty"`
|
||||||
|
Credit bool `json:"credit,omitempty"`
|
||||||
|
ErrorCodes []string `json:"error-codes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifier implements the Verifier interface
|
||||||
|
type verifier struct {
|
||||||
|
secretKey string
|
||||||
|
verifyURL string
|
||||||
|
timeout time.Duration
|
||||||
|
logger logger.Logger
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVerifier creates a new hCaptcha verifier
|
||||||
|
func NewVerifier(config *Config, log logger.Logger) Verifier {
|
||||||
|
verifyURL := config.VerifyURL
|
||||||
|
if verifyURL == "" {
|
||||||
|
verifyURL = DefaultVerifyURL
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := config.Timeout
|
||||||
|
if timeout == 0 {
|
||||||
|
timeout = DefaultTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
return &verifier{
|
||||||
|
secretKey: config.SecretKey,
|
||||||
|
verifyURL: verifyURL,
|
||||||
|
timeout: timeout,
|
||||||
|
logger: log,
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify verifies an hCaptcha token with the hCaptcha API
|
||||||
|
func (v *verifier) Verify(token string, remoteIP string) (*VerificationResponse, error) {
|
||||||
|
if token == "" {
|
||||||
|
return &VerificationResponse{
|
||||||
|
Success: false,
|
||||||
|
ErrorCodes: []string{"missing-input-response"},
|
||||||
|
}, fmt.Errorf("hCaptcha token is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare form data
|
||||||
|
data := url.Values{}
|
||||||
|
data.Set("secret", v.secretKey)
|
||||||
|
data.Set("response", token)
|
||||||
|
if remoteIP != "" {
|
||||||
|
data.Set("remoteip", remoteIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create request with form data in body
|
||||||
|
req, err := http.NewRequest("POST", v.verifyURL, strings.NewReader(data.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
v.logger.Error("Failed to create hCaptcha verification request", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to create verification request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
// Make request
|
||||||
|
resp, err := v.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
v.logger.Error("Failed to verify hCaptcha token", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to verify token: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Read response body
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
v.logger.Error("Failed to read hCaptcha verification response", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to read verification response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
var verificationResponse VerificationResponse
|
||||||
|
if err := json.Unmarshal(body, &verificationResponse); err != nil {
|
||||||
|
v.logger.Error("Failed to parse hCaptcha verification response", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
"body": string(body),
|
||||||
|
"status": resp.StatusCode,
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to parse verification response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log verification result
|
||||||
|
if verificationResponse.Success {
|
||||||
|
v.logger.Debug("hCaptcha token verified successfully", map[string]interface{}{
|
||||||
|
"hostname": verificationResponse.Hostname,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
v.logger.Warn("hCaptcha token verification failed", map[string]interface{}{
|
||||||
|
"error_codes": verificationResponse.ErrorCodes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &verificationResponse, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user