9a848e8c53
Implement a command palette accessible via CMD+K/Ctrl+K using the ninja-keys web component. Features include: - New /api/cmd-k endpoint serving dynamic CV entries (experiences, projects, courses) - Language-aware responses with 1-hour cache headers - Scroll-to-section functionality for quick navigation - Enhanced keyboard shortcuts modal with CMD+K documentation - Comprehensive test coverage for API and UI interactions Also includes cleanup of deprecated debug test files and various UI polish improvements to contact form, themes, and action bar components.
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.