949c9a0351
- Move docs/ contents to doc/ with proper numbering: - CONTACT-FORM-QUICKSTART.md → 17-CONTACT-FORM.md - SECURITY-AUDIT-REPORT.md → 18-SECURITY-AUDIT.md - SECURITY.md → 19-SECURITY-IMPLEMENTATION.md - Delete duplicate/redundant files from docs/: - CMD-K-COMMAND-BAR.md (duplicate of 16-CMD-K-API.md) - CONTACT_FORM_IMPLEMENTATION.md (overlaps with quickstart) - SECURITY-IMPLEMENTATION-SUMMARY.md (summary of audit) - Update doc/README.md with new document references - Update test counts to 39 test files across all READMEs - Update all "Last Updated" dates to 2025-12-01 - Add new API endpoints documentation (text, cmd-k, contact, toggles) - Update PROJECT-MEMORY.md with new features and correct paths
13 KiB
13 KiB
Contact Form Quick Start Guide
TL;DR
All security middleware is implemented and tested. You just need to:
- Create the contact handler
- Integrate an email service
- Add the route
- Create the HTML form
Step 1: Create Contact Handler
File: internal/handlers/contact.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, `<div class="error">%s</div>`, 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(`<div class="success">Message sent successfully! We'll get back to you soon.</div>`))
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
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Contact Form</title>
<!-- Include HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
.form-group { margin-bottom: 1rem; }
label { display: block; margin-bottom: 0.5rem; }
input, textarea { width: 100%; padding: 0.5rem; }
button { padding: 0.75rem 1.5rem; background: #0066cc; color: white; border: none; cursor: pointer; }
.error { color: red; padding: 1rem; background: #ffeeee; margin: 1rem 0; }
.success { color: green; padding: 1rem; background: #eeffee; margin: 1rem 0; }
.hidden { position: absolute; left: -9999px; }
</style>
</head>
<body>
<h1>Contact Me</h1>
<form id="contact-form"
hx-post="/api/contact"
hx-trigger="submit"
hx-target="#form-result"
hx-swap="innerHTML"
_="on htmx:afterRequest if event.detail.successful reset() me end">
<!-- CSRF Token (get from server) -->
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<!-- Timestamp (set by JavaScript) -->
<input type="hidden" name="timestamp" id="form-timestamp">
<!-- Honeypot field (hidden from humans, visible to bots) -->
<input type="text"
name="website"
id="website"
class="hidden"
tabindex="-1"
autocomplete="off"
aria-hidden="true">
<!-- Real Fields -->
<div class="form-group">
<label for="name">Name *</label>
<input type="text"
name="name"
id="name"
required
maxlength="100"
pattern="[\p{L}\s'\-]+"
title="Name can only contain letters, spaces, hyphens, and apostrophes">
</div>
<div class="form-group">
<label for="email">Email *</label>
<input type="email"
name="email"
id="email"
required
maxlength="254">
</div>
<div class="form-group">
<label for="company">Company</label>
<input type="text"
name="company"
id="company"
maxlength="100">
</div>
<div class="form-group">
<label for="subject">Subject *</label>
<input type="text"
name="subject"
id="subject"
required
maxlength="200"
pattern="[\p{L}\p{N}\s.,!?'"()\-:;#]+"
title="Subject can only contain letters, numbers, spaces, and basic punctuation">
</div>
<div class="form-group">
<label for="message">Message *</label>
<textarea name="message"
id="message"
required
maxlength="5000"
rows="6"></textarea>
</div>
<button type="submit">Send Message</button>
</form>
<div id="form-result"></div>
<script>
// Set timestamp when form loads (for bot detection)
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('form-timestamp').value = Math.floor(Date.now() / 1000);
});
</script>
</body>
</html>
Step 4: Generate CSRF Token in Handler
File: internal/handlers/contact.go (add page handler)
// 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:
mux.HandleFunc("/contact", contactHandler.ShowContactForm)
Step 5: Configure Email Service
Option 1: SMTP (Gmail, Office 365, etc.)
Environment variables:
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
SENDGRID_API_KEY=your-api-key
CONTACT_EMAIL=contact@yourdomain.com
Option 3: AWS SES
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
# 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
# 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: "<script>alert('XSS')</script>"
# Expected: HTML escaped in email
# Email Header Injection
# Subject: "Test\nBcc: attacker@evil.com"
# Expected: 400 Bad Request (invalid characters)
Security Monitoring
Check Logs
# 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
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
# /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
# 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:
- Create the contact handler (copy code above)
- Choose and configure an email service
- Add the routes
- Create the HTML form
Your contact form is now production-ready with comprehensive security controls.