package handlers import ( "fmt" "log" "net/http" "strings" "time" "github.com/juanatsap/cv-site/internal/constants" "github.com/juanatsap/cv-site/internal/services" "github.com/juanatsap/cv-site/internal/templates" ) // EmailSender is an interface for sending contact form emails // This allows for easy mocking in tests type EmailSender interface { SendContactForm(data *services.ContactFormData) error } // ContactHandler handles contact form submissions type ContactHandler struct { templates *templates.Manager emailService EmailSender } // NewContactHandler creates a new contact handler func NewContactHandler(tmpl *templates.Manager, emailService EmailSender) *ContactHandler { return &ContactHandler{ templates: tmpl, emailService: emailService, } } // ContactFormRequest represents the contact form submission type ContactFormRequest struct { Email string Name string Company string Subject string Message string Honeypot string // Hidden field - should be empty SubmitTime time.Time // Set by client, checked server-side CSRFToken string } // Submit handles POST /api/contact func (h *ContactHandler) Submit(w http.ResponseWriter, r *http.Request) { // Only accept POST if r.Method != http.MethodPost { HandleError(w, r, NewAppError(nil, "Method not allowed", http.StatusMethodNotAllowed, false)) return } // Parse form if err := r.ParseForm(); err != nil { log.Printf("ERROR parsing contact form: %v", err) h.renderError(w, r, "Invalid form data. Please try again.") return } // Extract form data req := &ContactFormRequest{ Email: strings.TrimSpace(r.FormValue("email")), Name: strings.TrimSpace(r.FormValue("name")), Company: strings.TrimSpace(r.FormValue("company")), Subject: strings.TrimSpace(r.FormValue("subject")), Message: strings.TrimSpace(r.FormValue("message")), Honeypot: r.FormValue("website"), // Honeypot field CSRFToken: r.FormValue("csrf_token"), } // Bot protection: Honeypot check if req.Honeypot != "" { log.Printf("SECURITY: Honeypot triggered from IP %s", getClientIP(r)) // Don't reveal that we detected a bot - just show success h.renderSuccess(w, r) return } // Bot protection: Timing check submitTimeStr := r.FormValue("submit_time") if submitTimeStr != "" { // Parse submit time (Unix timestamp in milliseconds) var submitTimeMs int64 if _, err := fmt.Sscanf(submitTimeStr, "%d", &submitTimeMs); err == nil { submitTime := time.Unix(0, submitTimeMs*int64(time.Millisecond)) elapsed := time.Since(submitTime) // Reject if submitted too fast (< 2 seconds) if elapsed < 2*time.Second { log.Printf("SECURITY: Form submitted too fast (%v) from IP %s", elapsed, getClientIP(r)) h.renderError(w, r, "Please take your time filling out the form.") return } } } // CSRF validation is handled by middleware // Validate required fields if req.Email == "" { h.renderError(w, r, "Email address is required.") return } if req.Message == "" { h.renderError(w, r, "Message is required.") return } // Create email data emailData := &services.ContactFormData{ Email: req.Email, Name: req.Name, Company: req.Company, Subject: req.Subject, Message: req.Message, IP: getClientIP(r), Time: time.Now(), } // Send email if err := h.emailService.SendContactForm(emailData); err != nil { log.Printf("ERROR sending contact email: %v", err) // Check if it's a validation error or server error if strings.Contains(err.Error(), "validation failed") { h.renderError(w, r, err.Error()) return } // Internal server error h.renderError(w, r, "Failed to send message. Please try again later.") return } // Log successful submission (without sensitive data) log.Printf("Contact form submitted successfully from %s (%s)", req.Email, getClientIP(r)) // Render success response h.renderSuccess(w, r) } // renderSuccess renders the success partial func (h *ContactHandler) renderSuccess(w http.ResponseWriter, r *http.Request) { w.Header().Set(constants.HeaderContentType, constants.ContentTypeHTML) w.WriteHeader(http.StatusOK) // Fallback HTML for when templates aren't available (e.g., in tests) fallbackHTML := `
Thank you for your message. I'll get back to you soon.
` + message + `