Need Immediate Assistance?
+Email: {{SUPPORT_EMAIL}}
+Website: {{COMPANY_URL}}
+Our team typically responds within 24 hours during business days.
+diff --git a/cmd/web/main.go b/cmd/web/main.go index bb9077f..c9a07cd 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -107,6 +107,7 @@ import ( "tm/internal/tender" "tm/internal/tender_approval" "tm/internal/user" + "tm/pkg/security" "tm/cmd/web/bootstrap" _ "tm/cmd/web/docs" // This is generated by swag @@ -143,6 +144,7 @@ func main() { // Initialize notification service notificationSDK := bootstrap.InitNotificationService(conf.Notification, logger) + xssPolicy := security.NewXSSPolicy() // Initialize hCaptcha verifier hcaptchaVerifier := bootstrap.InitHCaptchaService(conf.HCaptcha, logger) @@ -178,10 +180,10 @@ func main() { tenderService := tender.NewService(tenderRepository, companyService, logger) feedbackService := feedback.NewService(feedbackRepo, tenderService, companyService, logger) tenderApprovalService := tender_approval.NewService(tenderApprovalRepository, tenderService, logger) - inquiryService := inquiry.NewService(inquiryRepository, notificationSDK, logger) + inquiryService := inquiry.NewService(inquiryRepository, notificationSDK, logger, xssPolicy) flagService := assets.NewService(logger, conf.Assets.FlagsPath) notificationService := notification.NewService(notificationSDK, userService, customerService, logger) - contactService := contact.NewService(contactRepository, logger) + contactService := contact.NewService(contactRepository, logger, notificationSDK) cmsService := cms.NewService(cmsRepository, logger) scraperService := scraper.NewService(conf.Scraper.BaseURL, conf.Scraper.Timeout, logger, conf.Scraper.Username, conf.Scraper.Password, conf.Scraper.LoginURL) logger.Info("Services initialized successfully", map[string]interface{}{ @@ -196,7 +198,7 @@ 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, hcaptchaVerifier) + inquiryHandler := inquiry.NewHandler(inquiryService, logger, hcaptchaVerifier, xssPolicy) flagHandler := assets.NewHandler(flagService, logger) notificationHandler := notification.NewHandler(notificationService) contactHandler := contact.NewHandler(contactService, hcaptchaVerifier) @@ -210,8 +212,8 @@ func main() { e := bootstrap.InitHTTPServer(conf, logger) // Register routes - router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, scraperHandler) - router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler) + router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, scraperHandler, xssPolicy) + router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, xssPolicy) // Start server serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port) diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index c584990..aa59c12 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -14,13 +14,23 @@ import ( "tm/internal/tender" "tm/internal/tender_approval" "tm/internal/user" + "tm/pkg/security" "github.com/labstack/echo/v4" ) -func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, scraperHandler *scraper.Handler) { +func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, scraperHandler *scraper.Handler, xssPolicy *security.XSSPolicy) { adminV1 := e.Group("/admin/v1") + // Add CSP middleware to admin routes (stricter policy) + adminCSPConfig := security.DefaultCSPConfig() + adminCSPConfig.ScriptSrc = []string{"'self'"} // No unsafe-inline for admin + adminCSPConfig.StyleSrc = []string{"'self'"} // No unsafe-inline for admin + e.Use(security.CSPMiddleware(adminCSPConfig)) + + // Add CSP report endpoint + e.POST("/api/v1/csp-report", security.CSPReportHandler) + // Admin Users Routes adminUsersGP := adminV1.Group("/users") { @@ -175,9 +185,16 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler } } -func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler) { +func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, xssPolicy *security.XSSPolicy) { v1 := e.Group("/api/v1") + // Add CSP middleware to public routes + cspConfig := security.DefaultCSPConfig() + e.Use(security.CSPMiddleware(cspConfig)) + + // Add CSP report endpoint + e.POST("/api/v1/csp-report", security.CSPReportHandler) + customerGP := v1.Group("/profile") { customerGP.POST("/login", customerHandler.Login) diff --git a/go.mod b/go.mod index 122c2cd..e2db4ab 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.24.4 require ( github.com/golang-jwt/jwt/v5 v5.3.0 github.com/labstack/echo/v4 v4.13.4 + github.com/microcosm-cc/bluemonday v1.0.27 github.com/minio/minio-go/v7 v7.0.97 github.com/redis/go-redis/v9 v9.14.0 github.com/robfig/cron/v3 v3.0.1 @@ -21,6 +22,7 @@ require ( require ( github.com/KyleBanks/depth v1.2.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -32,6 +34,7 @@ require ( github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/cpuid/v2 v2.2.11 // indirect github.com/klauspost/crc32 v1.3.0 // indirect diff --git a/go.sum b/go.sum index 2eaa228..e8abd72 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -34,6 +36,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -57,6 +61,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q= github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= diff --git a/internal/contact/email_template.go b/internal/contact/email_template.go new file mode 100644 index 0000000..f7952af --- /dev/null +++ b/internal/contact/email_template.go @@ -0,0 +1,349 @@ +package contact + +import ( + "fmt" + "strings" + "time" +) + +// ContactEmailTemplateData represents the data needed for contact confirmation email +type ContactEmailTemplateData struct { + FullName string + Email string + Phone string + Message string + ContactID string + SubmittedAt int64 + CompanyName string + CompanyURL string + SupportEmail string +} + +// GenerateContactConfirmationEmailTemplate generates a beautiful HTML email template for contact confirmation +func GenerateContactConfirmationEmailTemplate(data *ContactEmailTemplateData) string { + // Format the submission date + submittedAt := time.Unix(data.SubmittedAt, 0).Format("January 2, 2006 at 3:04 PM") + + html := ` + + +
+ + +Your message has been received and we'll get back to you soon.
+Email: {{SUPPORT_EMAIL}}
+Website: {{COMPANY_URL}}
+Our team typically responds within 24 hours during business days.
+