package handlers import ( "fmt" "log" "net/http" "strings" "time" "github.com/juanatsap/cv-site/internal/services" "github.com/juanatsap/cv-site/internal/templates" ) // ContactHandler handles contact form submissions type ContactHandler struct { templates *templates.Manager emailService *services.EmailService } // NewContactHandler creates a new contact handler func NewContactHandler(tmpl *templates.Manager, emailService *services.EmailService) *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("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) tmpl, err := h.templates.Render("contact_success.html") if err != nil { log.Printf("ERROR loading success template: %v", err) // Fallback to simple HTML _, _ = w.Write([]byte(`

Message Sent!

Thank you for your message. I'll get back to you soon.

`)) return } if err := tmpl.Execute(w, nil); err != nil { log.Printf("ERROR rendering success template: %v", err) _, _ = w.Write([]byte(`

Message Sent!

Thank you for your message. I'll get back to you soon.

`)) } } // renderError renders the error partial func (h *ContactHandler) renderError(w http.ResponseWriter, r *http.Request, message string) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusBadRequest) data := map[string]interface{}{ "Message": message, } tmpl, err := h.templates.Render("contact_error.html") if err != nil { log.Printf("ERROR loading error template: %v", err) // Fallback to simple HTML _, _ = w.Write([]byte(`

Error

` + message + `

`)) return } if err := tmpl.Execute(w, data); err != nil { log.Printf("ERROR rendering error template: %v", err) _, _ = w.Write([]byte(`

Error

` + message + `

`)) } } // getClientIP extracts the client IP address from the request func getClientIP(r *http.Request) string { // Check X-Forwarded-For header (for proxies) ip := r.Header.Get("X-Forwarded-For") if ip != "" { // X-Forwarded-For can contain multiple IPs, take the first one ips := strings.Split(ip, ",") return strings.TrimSpace(ips[0]) } // Check X-Real-IP header ip = r.Header.Get("X-Real-IP") if ip != "" { return ip } // Fallback to RemoteAddr ip = r.RemoteAddr // Remove port if present if colonIndex := strings.LastIndex(ip, ":"); colonIndex != -1 { ip = ip[:colonIndex] } return ip }