Merge branch 'develop' into TM-295

This commit is contained in:
Nima Nakhsotin
2025-12-10 14:28:04 +03:30
9 changed files with 224 additions and 20 deletions
+19
View File
@@ -5,6 +5,7 @@ import (
"time"
"tm/pkg/authorization"
"tm/pkg/config"
"tm/pkg/hcaptcha"
"tm/pkg/logger"
"tm/pkg/mongo"
"tm/pkg/notification"
@@ -249,3 +250,21 @@ func InitOllamaService(conf config.OllamaConfig, log logger.Logger) ollama.SDK {
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
}
+1
View File
@@ -15,6 +15,7 @@ type Config struct {
Assets config.AssetsConfig
Notification config.NotificationConfig
Ollama config.OllamaConfig
HCaptcha config.HCaptchaConfig
Scraper ScraperConfig
UserAuth AuthConfig
CustomerAuth AuthConfig
+5 -2
View File
@@ -146,6 +146,9 @@ func main() {
notificationSDK := bootstrap.InitNotificationService(conf.Notification, logger)
xssPolicy := security.NewXSSPolicy()
// Initialize hCaptcha verifier
hcaptchaVerifier := bootstrap.InitHCaptchaService(conf.HCaptcha, logger)
// Initialize authorization service
userAuthService := bootstrap.InitAuthorizationService(conf.UserAuth.JWT, redisClient, logger)
customerAuthService := bootstrap.InitAuthorizationService(conf.CustomerAuth.JWT, redisClient, logger)
@@ -195,10 +198,10 @@ func main() {
tenderHandler := tender.NewHandler(tenderService, logger)
feedbackHandler := feedback.NewHandler(feedbackService, userHandler, customerHandler, logger)
tenderApprovalHandler := tender_approval.NewHandler(tenderApprovalService, logger)
inquiryHandler := inquiry.NewHandler(inquiryService, logger, xssPolicy)
inquiryHandler := inquiry.NewHandler(inquiryService, logger, hcaptchaVerifier, xssPolicy)
flagHandler := assets.NewHandler(flagService, logger)
notificationHandler := notification.NewHandler(notificationService)
contactHandler := contact.NewHandler(contactService)
contactHandler := contact.NewHandler(contactService, hcaptchaVerifier)
cmsHandler := cms.NewHandler(cmsService)
scraperHandler := scraper.NewHandler(scraperService)
logger.Info("Handlers initialized successfully", map[string]interface{}{
+5 -4
View File
@@ -4,10 +4,11 @@ import "tm/pkg/response"
// CreateContactForm represents the form for creating a new contact message
type CreateContactForm struct {
FullName string `json:"full_name" valid:"required,length(2|100)" example:"John Doe"`
Email string `json:"email" valid:"required,email" example:"john.doe@example.com"`
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"`
FullName string `json:"full_name" valid:"required,length(2|100)" example:"John Doe"`
Email string `json:"email" valid:"required,email" example:"john.doe@example.com"`
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"`
HCaptchaToken string `json:"hcaptcha_token" valid:"required" example:"P0_eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."`
}
// UpdateContactStatusForm represents the form for updating contact status (admin only)
+17 -3
View File
@@ -1,6 +1,7 @@
package contact
import (
"tm/pkg/hcaptcha"
"tm/pkg/response"
"github.com/labstack/echo/v4"
@@ -8,13 +9,15 @@ import (
// Handler handles HTTP requests for contact operations
type Handler struct {
service Service
service Service
hcaptchaVerifier hcaptcha.Verifier
}
// NewHandler creates a new contact handler
func NewHandler(service Service) *Handler {
func NewHandler(service Service, hcaptchaVerifier hcaptcha.Verifier) *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())
}
// 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
contact, err := h.service.Create(c.Request().Context(), form)
if err != nil {
+5 -4
View File
@@ -11,10 +11,11 @@ import (
// CreateInquiryForm represents the data required to create a new inquiry
type CreateInquiryForm struct {
FullName string `json:"full_name" valid:"required,length(2|100)" example:"John Doe"` // Full name of the inquirer
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
PhoneNumber string `json:"phone_number" valid:"required,length(10|20)" example:"+1234567890"` // Phone number
FullName string `json:"full_name" valid:"required,length(2|100)" example:"John Doe"` // Full name of the inquirer
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
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
+21 -7
View File
@@ -2,6 +2,7 @@ package inquiry
import (
"encoding/json"
"tm/pkg/hcaptcha"
"tm/pkg/logger"
"tm/pkg/response"
"tm/pkg/security"
@@ -11,17 +12,19 @@ import (
// Handler handles HTTP requests for inquiry operations
type Handler struct {
service Service
logger logger.Logger
xssPolicy *security.XSSPolicy
service Service
logger logger.Logger
hcaptchaVerifier hcaptcha.Verifier
xssPolicy *security.XSSPolicy
}
// NewInquiryHandler creates a new inquiry handler
func NewHandler(service Service, logger logger.Logger, xssPolicy *security.XSSPolicy) *Handler {
func NewHandler(service Service, logger logger.Logger, hcaptchaVerifier hcaptcha.Verifier, xssPolicy *security.XSSPolicy) *Handler {
return &Handler{
service: service,
logger: logger,
xssPolicy: xssPolicy,
service: service,
logger: logger,
hcaptchaVerifier: hcaptchaVerifier,
xssPolicy: xssPolicy,
}
}
@@ -49,6 +52,17 @@ func (h *Handler) CreateInquiry(c echo.Context) 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
inquiry, err := h.service.Create(c.Request().Context(), form)
if err != nil {
+6
View File
@@ -89,6 +89,12 @@ type OllamaConfig struct {
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
// OS environment variables take precedence over .env file values
func LoadConfig[T any](path string, config T) (T, error) {
+145
View File
@@ -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
}