package handlers import ( "fmt" "log" "net/http" "strconv" "strings" "time" uimodel "github.com/juanatsap/cv-site/internal/models/ui" ) // ============================================================================== // CONTACT FORM SUBMISSION HANDLER // Part of CVHandler - handles POST /api/contact // ============================================================================== // ContactFormData represents the contact form submission type ContactFormData struct { Email string Name string Company string Subject string Message string Website string // Honeypot field - should be empty FormLoadedAt string // Timing field - Unix timestamp in milliseconds Lang string } // HandleContact handles contact form submissions func (h *CVHandler) HandleContact(w http.ResponseWriter, r *http.Request) { // Only accept POST requests if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Parse form data if err := r.ParseForm(); err != nil { log.Printf("Error parsing contact form: %v", err) h.renderContactError(w, r, "Invalid form data. Please try again.") return } // Get language from query parameter lang := r.URL.Query().Get("lang") if lang == "" { lang = "en" } // Validate language if lang != "en" && lang != "es" { lang = "en" } // Extract form data formData := &ContactFormData{ 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")), Website: r.FormValue("website"), // Honeypot FormLoadedAt: r.FormValue("form_loaded_at"), // Timing Lang: lang, } // Validate form data (includes bot protection) if err := validateContactForm(formData, r); err != nil { log.Printf("Contact form validation failed from IP %s: %v", getClientIP(r), err) // Don't reveal specific errors to potential bots if strings.Contains(err.Error(), "spam detected") { // Silently succeed for bots h.renderContactSuccess(w, r, lang) return } h.renderContactError(w, r, err.Error()) return } // Log the contact form submission (in production, send email or save to database) log.Printf("Contact form submission from %s (IP: %s)", formData.Email, getClientIP(r)) log.Printf(" Name: %s, Company: %s", formData.Name, formData.Company) log.Printf(" Subject: %s", formData.Subject) log.Printf(" Message length: %d characters", len(formData.Message)) // TODO: Implement actual email sending or database storage here // For now, we just log and return success // Render success response h.renderContactSuccess(w, r, lang) } // validateContactForm validates the contact form data and performs bot protection func validateContactForm(data *ContactFormData, r *http.Request) error { // Bot protection: Honeypot check - website field should be empty if data.Website != "" { return fmt.Errorf("spam detected: honeypot field filled") } // Bot protection: Timing check - form should take at least 2 seconds to fill if data.FormLoadedAt != "" { loadedAt, err := strconv.ParseInt(data.FormLoadedAt, 10, 64) if err == nil { now := time.Now().UnixMilli() elapsed := now - loadedAt // Form filled too quickly (< 2 seconds) - likely a bot if elapsed < 2000 { return fmt.Errorf("spam detected: form filled too quickly (%dms)", elapsed) } // Form took too long (> 1 hour) - timestamp expired if elapsed > 3600000 { return fmt.Errorf("form session expired, please refresh and try again") } } } // Required field validation if data.Email == "" { return fmt.Errorf("email address is required") } if data.Message == "" { return fmt.Errorf("message is required") } // Email format validation (basic) if !strings.Contains(data.Email, "@") || !strings.Contains(data.Email, ".") { return fmt.Errorf("invalid email format") } // Message length validation if len(data.Message) < 10 { return fmt.Errorf("message is too short (minimum 10 characters)") } if len(data.Message) > 5000 { return fmt.Errorf("message is too long (maximum 5000 characters)") } return nil } // renderContactSuccess renders the contact success partial func (h *CVHandler) renderContactSuccess(w http.ResponseWriter, r *http.Request, lang string) { // Load UI data for the specified language ui, err := uimodel.LoadUI(lang) if err != nil { log.Printf("Error loading UI data: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } // Create template data data := map[string]interface{}{ "UI": ui, "Lang": lang, } // Render the success template w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) tmpl, err := h.templates.Render("contact-success") if err != nil { log.Printf("Error loading contact success template: %v", err) // Fallback to simple HTML _, _ = w.Write([]byte(`
`)) return } if err := tmpl.Execute(w, data); err != nil { log.Printf("Error rendering contact success template: %v", err) _, _ = w.Write([]byte(``)) } } // renderContactError renders the contact error partial func (h *CVHandler) renderContactError(w http.ResponseWriter, r *http.Request, errorMessage string) { // Get language from query parameter lang := r.URL.Query().Get("lang") if lang == "" { lang = "en" } // Validate language if lang != "en" && lang != "es" { lang = "en" } // Load UI data for the specified language ui, err := uimodel.LoadUI(lang) if err != nil { log.Printf("Error loading UI data: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } // Create template data data := map[string]interface{}{ "UI": ui, "Lang": lang, "ErrorMessage": errorMessage, } // Render the error template w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusBadRequest) tmpl, err := h.templates.Render("contact-error") if err != nil { log.Printf("Error loading contact error template: %v", err) // Fallback to simple HTML _, _ = w.Write([]byte(``)) return } if err := tmpl.Execute(w, data); err != nil { log.Printf("Error rendering contact error template: %v", err) _, _ = w.Write([]byte(``)) } } // Note: getClientIP is defined in contact.go