# Contact Form Quick Start Guide ## TL;DR All security middleware is implemented and tested. You just need to: 1. Create the contact handler 2. Integrate an email service 3. Add the route 4. Create the HTML form --- ## Step 1: Create Contact Handler **File:** `internal/handlers/contact.go` ```go package handlers import ( "encoding/json" "fmt" "log" "net/http" "time" "github.com/juanatsap/cv-site/internal/middleware" "github.com/juanatsap/cv-site/internal/validation" ) type ContactHandler struct { // Add email service here when you choose one // emailService EmailService } func NewContactHandler() *ContactHandler { return &ContactHandler{} } // SendMessage handles contact form submissions func (h *ContactHandler) SendMessage(w http.ResponseWriter, r *http.Request) { // 1. Parse JSON request var req validation.ContactFormRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { middleware.LogSecurityEvent(middleware.EventValidationFailed, r, "Invalid JSON: "+err.Error()) http.Error(w, "Invalid request format", http.StatusBadRequest) return } // 2. Set server timestamp (don't trust client) req.Timestamp = time.Now().Unix() // 3. Validate input if err := validation.ValidateContactForm(&req); err != nil { middleware.LogSecurityEvent(middleware.EventValidationFailed, r, err.Error()) // Return user-friendly error for HTMX if r.Header.Get("HX-Request") != "" { w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, `
%s
`, err.Error()) return } http.Error(w, err.Error(), http.StatusBadRequest) return } // 4. Sanitize content (removes HTML, normalizes whitespace) validation.SanitizeContactForm(&req) // 5. Send email if err := h.sendEmail(&req); err != nil { middleware.LogSecurityEvent(middleware.EventEmailSendFailed, r, err.Error()) http.Error(w, "Failed to send message. Please try again later.", http.StatusInternalServerError) return } // 6. Log success middleware.LogSecurityEvent(middleware.EventContactFormSent, r, fmt.Sprintf("From: %s <%s>", req.Name, req.Email)) // 7. Return success if r.Header.Get("HX-Request") != "" { w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusOK) w.Write([]byte(`
Message sent successfully! We'll get back to you soon.
`)) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{ "message": "Message sent successfully", }) } // sendEmail sends the contact form email // TODO: Choose an email service and implement this func (h *ContactHandler) sendEmail(req *validation.ContactFormRequest) error { // OPTION 1: SMTP (using net/smtp) // return h.sendViaSMTP(req) // OPTION 2: SendGrid API // return h.sendViaSendGrid(req) // OPTION 3: AWS SES // return h.sendViaAWSSES(req) // OPTION 4: Mailgun API // return h.sendViaMailgun(req) // For now, just log it (replace with actual implementation) log.Printf("EMAIL: From: %s <%s>, Subject: %s\n%s", req.Name, req.Email, req.Subject, req.Message) return nil } // Example SMTP implementation /* import "net/smtp" func (h *ContactHandler) sendViaSMTP(req *validation.ContactFormRequest) error { // Load SMTP config from environment host := os.Getenv("SMTP_HOST") port := os.Getenv("SMTP_PORT") user := os.Getenv("SMTP_USER") pass := os.Getenv("SMTP_PASS") from := os.Getenv("SMTP_FROM") to := os.Getenv("CONTACT_EMAIL") // Set up authentication auth := smtp.PlainAuth("", user, pass, host) // Build email subject := "Contact Form: " + req.Subject body := fmt.Sprintf(`From: %s <%s> Company: %s %s --- Sent via contact form on %s `, req.Name, req.Email, req.Company, req.Message, time.Now().Format("2006-01-02 15:04:05")) msg := []byte(fmt.Sprintf(`To: %s From: %s Reply-To: %s Subject: %s Content-Type: text/plain; charset=UTF-8 %s`, to, from, req.Email, subject, body)) // Send email return smtp.SendMail(host+":"+port, auth, from, []string{to}, msg) } */ ``` --- ## Step 2: Add Route **File:** `internal/routes/routes.go` ```go func Setup(cvHandler *handlers.CVHandler, healthHandler *handlers.HealthHandler) http.Handler { mux := http.NewServeMux() // ... existing routes ... // Contact form endpoint - FULLY PROTECTED contactHandler := handlers.NewContactHandler() csrf := middleware.NewCSRFProtection() contactRateLimiter := middleware.NewContactRateLimiter() protectedContactHandler := middleware.BrowserOnly( csrf.Middleware( contactRateLimiter.Middleware( http.HandlerFunc(contactHandler.SendMessage), ), ), ) mux.Handle("/api/contact", protectedContactHandler) // ... rest of middleware chain ... return handler } ``` --- ## Step 3: Create HTML Form Template **File:** `templates/contact.html` ```html Contact Form

Contact Me

``` --- ## Step 4: Generate CSRF Token in Handler **File:** `internal/handlers/contact.go` (add page handler) ```go // ShowContactForm displays the contact form with CSRF token func (h *ContactHandler) ShowContactForm(w http.ResponseWriter, r *http.Request) { // Get or generate CSRF token csrf := middleware.NewCSRFProtection() token, err := csrf.GetToken(w, r) if err != nil { http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError) return } // Render template with CSRF token data := map[string]interface{}{ "CSRFToken": token, } // Use your template manager to render // h.templates.Render(w, "contact.html", data) } ``` **Add route:** ```go mux.HandleFunc("/contact", contactHandler.ShowContactForm) ``` --- ## Step 5: Configure Email Service ### Option 1: SMTP (Gmail, Office 365, etc.) **Environment variables:** ```bash SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_USER=your-email@gmail.com SMTP_PASS=your-app-specific-password SMTP_FROM=noreply@yourdomain.com CONTACT_EMAIL=contact@yourdomain.com ``` ### Option 2: SendGrid ```bash SENDGRID_API_KEY=your-api-key CONTACT_EMAIL=contact@yourdomain.com ``` ### Option 3: AWS SES ```bash AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=your-access-key AWS_SECRET_ACCESS_KEY=your-secret-key CONTACT_EMAIL=contact@yourdomain.com ``` --- ## Testing Checklist ### 1. Manual Testing ```bash # Test valid submission (browser required) # Fill out form on http://localhost:1999/contact # Test CSRF protection curl -X POST http://localhost:1999/api/contact \ -H "Content-Type: application/json" \ -d '{"name":"Test","email":"test@example.com","subject":"Test","message":"Test"}' # Expected: 403 Forbidden (missing CSRF token or browser headers) # Test rate limiting (submit 6 times within an hour) # Expected: 6th submission returns 429 Too Many Requests # Test bot detection - honeypot # Fill the hidden "website" field # Expected: Validation error # Test bot detection - timing # Submit form immediately after page load # Expected: Validation error # Test email injection # Try: name="Test\nBcc: attacker@evil.com" # Expected: Validation error ``` ### 2. Attack Simulations ```bash # SQL Injection curl -X POST http://localhost:1999/api/contact \ -H "Origin: http://localhost:1999" \ -H "X-Requested-With: XMLHttpRequest" \ -H "Cookie: csrf_token=..." \ -d '{"name":"Robert\"; DROP TABLE users; --","email":"test@example.com",...}' # Expected: 400 Bad Request (invalid name format) # XSS # Message: "" # Expected: HTML escaped in email # Email Header Injection # Subject: "Test\nBcc: attacker@evil.com" # Expected: 400 Bad Request (invalid characters) ``` --- ## Security Monitoring ### Check Logs ```bash # View security events tail -f /var/log/cv-app/security.log # Filter by severity tail -f /var/log/cv-app/security.log | jq 'select(.severity == "HIGH")' # Count blocked requests grep "BLOCKED" /var/log/cv-app/security.log | wc -l # See who's trying to attack grep "BLOCKED" /var/log/cv-app/security.log | jq -r '.ip' | sort | uniq -c | sort -rn ``` --- ## Troubleshooting ### "CSRF validation failed" - Make sure CSRF token is being generated and included in form - Check cookie is being set with correct domain - Verify token in cookie matches token in form ### "Forbidden: Browser access only" - Ensure Origin or Referer header is present - Check ALLOWED_ORIGINS environment variable - Verify X-Requested-With header is set by HTMX ### "Rate limit exceeded" - Wait 1 hour and try again - Check if IP is correctly extracted (X-Forwarded-For) - Verify rate limit configuration (5 per hour) ### "Bot detected" - Don't fill the honeypot field (id="website") - Wait at least 2 seconds before submitting - Ensure timestamp is set correctly --- ## Production Deployment ### 1. Set Environment Variables ```bash GO_ENV=production ALLOWED_ORIGINS=juan.andres.morenorub.io SMTP_HOST=... SMTP_PORT=587 SMTP_USER=... SMTP_PASS=... CONTACT_EMAIL=... ``` ### 2. Configure Nginx Rate Limiting ```nginx # /etc/nginx/sites-available/cv-app limit_req_zone $binary_remote_addr zone=contact:10m rate=5r/h; location /api/contact { limit_req zone=contact burst=1 nodelay; proxy_pass http://127.0.0.1:1999; # ... other proxy settings ... } ``` ### 3. Set Up Monitoring ```bash # Configure fail2ban for repeated attacks # See SECURITY-AUDIT-REPORT.md for details # Set up log rotation sudo vi /etc/logrotate.d/cv-app # Configure alerts (Prometheus/Grafana) # Monitor rate_limit_violations, csrf_violations, etc. ``` --- ## That's It! 🎉 All security middleware is already implemented and tested: - ✅ CSRF protection - ✅ Origin validation (browser-only) - ✅ Input validation & sanitization - ✅ Rate limiting (5/hour) - ✅ Bot detection (honeypot + timing) - ✅ Email header injection prevention - ✅ Security logging You just need to: 1. Create the contact handler (copy code above) 2. Choose and configure an email service 3. Add the routes 4. Create the HTML form **Your contact form is now production-ready with comprehensive security controls.**