2c7f8de242
- Create internal/constants package with all hardcoded values (environment, cookies, themes, headers, routes, cache) - Create internal/httputil package for HTTP helper functions - Update all handlers and middleware to use centralized constants - Reorganize documentation with numbered prefixes (00-26) - Remove duplicate docs from validation folder and docs/ - Delete handlers/constants.go (moved to internal/constants)
1204 lines
28 KiB
Markdown
1204 lines
28 KiB
Markdown
# Go Routes and API Documentation
|
|
|
|
## Overview
|
|
|
|
The CV site uses Go's standard `net/http` package with a custom routing setup in `internal/routes/routes.go`. The routing system applies a comprehensive middleware chain for security, logging, caching, and preferences management.
|
|
|
|
### Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Middleware Chain │
|
|
│ │
|
|
│ Request → Recovery → Logger → SecurityHeaders │
|
|
│ ↓ ↓ ↓ │
|
|
│ DynamicCache → Preferences → Router → Handler │
|
|
│ ↓ │
|
|
│ ┌───────────────┐ │
|
|
│ │ Mux Routes │ │
|
|
│ │ - Public │ │
|
|
│ │ - HTMX │ │
|
|
│ │ - API │ │
|
|
│ │ - Protected │ │
|
|
│ └───────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Protected Endpoints │
|
|
│ │
|
|
│ /api/contact: │
|
|
│ BrowserOnly → RateLimiter(5/hour) → Handler │
|
|
│ │
|
|
│ /export/pdf: │
|
|
│ OriginChecker → RateLimiter(3/min) → Handler │
|
|
│ │
|
|
│ /static/*: │
|
|
│ CacheControl → FileServer │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## Route Table
|
|
|
|
### Public Routes
|
|
|
|
| Route | Method | Handler | Description | Response Type |
|
|
|-------|--------|---------|-------------|---------------|
|
|
| `/` | GET | `cvHandler.Home` | Main CV page with full layout | HTML |
|
|
| `/cv` | GET | `cvHandler.CVContent` | CV content partial (HTMX) | HTML (partial) |
|
|
| `/text` | GET | `cvHandler.PlainText` | Plain text CV for curl/AI | text/plain |
|
|
| `/health` | GET | `healthHandler.Check` | Health check endpoint | JSON |
|
|
|
|
#### `/` - Home Page
|
|
|
|
**Purpose:** Serves the main CV page with full HTML layout
|
|
|
|
**Handler:** `cvHandler.Home`
|
|
|
|
**Middleware:**
|
|
- Recovery
|
|
- Logger
|
|
- SecurityHeaders
|
|
- DynamicCacheControl
|
|
- PreferencesMiddleware
|
|
|
|
**Response:**
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>Juan's CV</title>
|
|
<!-- CSS, meta tags, etc. -->
|
|
</head>
|
|
<body>
|
|
<!-- Full page layout with header, CV content, footer -->
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
**Status Codes:**
|
|
- `200 OK` - Success
|
|
- `500 Internal Server Error` - Template error
|
|
|
|
---
|
|
|
|
#### `/cv` - CV Content Partial
|
|
|
|
**Purpose:** Returns CV content fragment for HTMX swaps
|
|
|
|
**Handler:** `cvHandler.CVContent`
|
|
|
|
**Query Parameters:**
|
|
- `lang` - Language (en/es)
|
|
- `length` - CV length (short/full)
|
|
- `icons` - Show icons (true/false)
|
|
|
|
**Response:**
|
|
```html
|
|
<section id="cv-content">
|
|
<!-- CV sections without layout -->
|
|
<div class="experience">...</div>
|
|
<div class="education">...</div>
|
|
</section>
|
|
```
|
|
|
|
**HTMX Usage:**
|
|
```html
|
|
<button hx-get="/cv?lang=es" hx-target="#cv-content">Spanish</button>
|
|
```
|
|
|
|
---
|
|
|
|
#### `/text` - Plain Text CV
|
|
|
|
**Purpose:** curl-friendly and AI-readable plain text CV
|
|
|
|
**Handler:** `cvHandler.PlainText`
|
|
|
|
**Response:**
|
|
```
|
|
JUAN ANDRÉS MORENO RUBIO
|
|
========================
|
|
|
|
Software Engineer | Go, HTMX, Cloud Architecture
|
|
|
|
EXPERIENCE
|
|
----------
|
|
Senior Software Engineer @ Company (2020-Present)
|
|
- Achievement 1
|
|
- Achievement 2
|
|
|
|
...
|
|
```
|
|
|
|
**Usage:**
|
|
```bash
|
|
curl https://juan.andres.morenorub.io/text
|
|
```
|
|
|
|
**Status Codes:**
|
|
- `200 OK` - Success
|
|
- `500 Internal Server Error` - Generation error
|
|
|
|
---
|
|
|
|
#### `/health` - Health Check
|
|
|
|
**Purpose:** Service health monitoring for load balancers
|
|
|
|
**Handler:** `healthHandler.Check`
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": "healthy",
|
|
"timestamp": "2025-12-06T10:30:00Z",
|
|
"version": "1.0.0"
|
|
}
|
|
```
|
|
|
|
**Status Codes:**
|
|
- `200 OK` - Service healthy
|
|
- `503 Service Unavailable` - Service degraded
|
|
|
|
### HTMX Interactive Endpoints
|
|
|
|
| Route | Method | Handler | Description | Response Type |
|
|
|-------|--------|---------|-------------|---------------|
|
|
| `/switch-language` | GET | `cvHandler.SwitchLanguage` | Toggle EN/ES language | HTML (partial) |
|
|
| `/toggle/length` | GET | `cvHandler.ToggleLength` | Toggle short/full CV | HTML (partial) |
|
|
| `/toggle/icons` | GET | `cvHandler.ToggleIcons` | Toggle icon display | HTML (partial) |
|
|
| `/toggle/theme` | GET | `cvHandler.ToggleTheme` | Toggle light/dark theme | HTML (partial) |
|
|
|
|
#### `/switch-language` - Language Toggle
|
|
|
|
**Purpose:** Switch between English and Spanish
|
|
|
|
**Handler:** `cvHandler.SwitchLanguage`
|
|
|
|
**Mechanism:**
|
|
1. Reads current language from preferences cookie
|
|
2. Toggles `en` ↔ `es`
|
|
3. Sets new preference cookie
|
|
4. Returns updated CV content
|
|
|
|
**Response:**
|
|
```html
|
|
<section id="cv-content" hx-swap-oob="true">
|
|
<!-- CV content in new language -->
|
|
</section>
|
|
<button id="lang-toggle" hx-swap-oob="true">
|
|
ES <!-- Shows opposite language -->
|
|
</button>
|
|
```
|
|
|
|
**HTMX Trigger:**
|
|
```html
|
|
<button hx-get="/switch-language" hx-target="body" hx-swap="outerHTML">
|
|
{{ if eq .Language "en" }}ES{{ else }}EN{{ end }}
|
|
</button>
|
|
```
|
|
|
|
---
|
|
|
|
#### `/toggle/length` - CV Length Toggle
|
|
|
|
**Purpose:** Switch between short (1-page) and full (detailed) CV
|
|
|
|
**Handler:** `cvHandler.ToggleLength`
|
|
|
|
**States:**
|
|
- `short` - Essential experience only (1 page)
|
|
- `full` - Complete experience history
|
|
|
|
**Response:**
|
|
```html
|
|
<section id="cv-content" hx-swap-oob="true">
|
|
<!-- CV with different level of detail -->
|
|
</section>
|
|
```
|
|
|
|
**Usage:**
|
|
```html
|
|
<button hx-get="/toggle/length" hx-target="#cv-content">
|
|
{{ if eq .Length "short" }}Full CV{{ else }}Short CV{{ end }}
|
|
</button>
|
|
```
|
|
|
|
---
|
|
|
|
#### `/toggle/icons` - Icon Display Toggle
|
|
|
|
**Purpose:** Show/hide skill and technology icons
|
|
|
|
**Handler:** `cvHandler.ToggleIcons`
|
|
|
|
**States:**
|
|
- `true` - Show icons (visual)
|
|
- `false` - Hide icons (text only, PDF-friendly)
|
|
|
|
**Response:**
|
|
```html
|
|
<section id="cv-content" hx-swap-oob="true">
|
|
<!-- CV with/without icon sprites -->
|
|
</section>
|
|
```
|
|
|
|
---
|
|
|
|
#### `/toggle/theme` - Theme Toggle
|
|
|
|
**Purpose:** Switch between light and dark mode
|
|
|
|
**Handler:** `cvHandler.ToggleTheme`
|
|
|
|
**States:**
|
|
- `light` - Light theme
|
|
- `dark` - Dark theme
|
|
|
|
**Response:**
|
|
```html
|
|
<html class="dark" hx-swap-oob="true">
|
|
<!-- Class changed on root element -->
|
|
</html>
|
|
```
|
|
|
|
**HTMX Trigger:**
|
|
```html
|
|
<button hx-get="/toggle/theme" hx-target="html" hx-swap="outerHTML">
|
|
{{ if eq .Theme "dark" }}☀️{{ else }}🌙{{ end }}
|
|
</button>
|
|
```
|
|
|
|
### API Endpoints
|
|
|
|
| Route | Method | Handler | Description | Security |
|
|
|-------|--------|---------|-------------|----------|
|
|
| `/api/cmd-k` | GET | `cvHandler.CmdKData` | Command palette data | Standard chain |
|
|
| `/api/contact` | POST | `cvHandler.HandleContact` | Contact form submission | BrowserOnly + RateLimit(5/hour) |
|
|
|
|
#### `/api/cmd-k` - Command Palette Data
|
|
|
|
**Purpose:** Provides search data for CMD+K command palette
|
|
|
|
**Handler:** `cvHandler.CmdKData`
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"commands": [
|
|
{
|
|
"id": "view-experience",
|
|
"title": "View Experience",
|
|
"description": "Jump to experience section",
|
|
"action": "#experience"
|
|
},
|
|
{
|
|
"id": "download-pdf",
|
|
"title": "Download PDF",
|
|
"description": "Export CV as PDF",
|
|
"action": "/export/pdf"
|
|
},
|
|
{
|
|
"id": "switch-language",
|
|
"title": "Switch to Spanish",
|
|
"description": "Change language to Spanish",
|
|
"action": "/switch-language"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Status Codes:**
|
|
- `200 OK` - Success
|
|
|
|
---
|
|
|
|
#### `/api/contact` - Contact Form Submission
|
|
|
|
**Purpose:** Handle contact form submissions with comprehensive security
|
|
|
|
**Handler:** `cvHandler.HandleContact`
|
|
|
|
**Method:** `POST`
|
|
|
|
**Security Chain:**
|
|
```
|
|
BrowserOnly → RateLimiter(5/hour) → Handler
|
|
```
|
|
|
|
**Request Body:**
|
|
```json
|
|
{
|
|
"name": "Juan José",
|
|
"email": "juan@example.com",
|
|
"company": "ACME Corp",
|
|
"subject": "Job Opportunity",
|
|
"message": "I'd like to discuss...",
|
|
"website": "",
|
|
"timestamp": 1701867000
|
|
}
|
|
```
|
|
|
|
**Validation Rules:**
|
|
- `name`: Required, max 100 chars, letters/spaces/hyphens only
|
|
- `email`: Required, max 254 chars, valid RFC 5322 email
|
|
- `company`: Optional, max 100 chars
|
|
- `subject`: Required, max 200 chars
|
|
- `message`: Required, max 5000 chars
|
|
- `website`: Honeypot (must be empty)
|
|
- `timestamp`: Must be 2s-24h old
|
|
|
|
**Success Response (200 OK):**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Message sent successfully"
|
|
}
|
|
```
|
|
|
|
**Error Response (400 Bad Request):**
|
|
```json
|
|
{
|
|
"success": false,
|
|
"errors": [
|
|
{
|
|
"field": "email",
|
|
"message": "Invalid email address format"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Error Response (429 Too Many Requests):**
|
|
```html
|
|
<div class="alert alert-error">
|
|
<h3>Too Many Requests</h3>
|
|
<p>You've submitted too many contact forms. Please wait an hour before trying again.</p>
|
|
</div>
|
|
```
|
|
|
|
**Status Codes:**
|
|
- `200 OK` - Success
|
|
- `400 Bad Request` - Validation error
|
|
- `403 Forbidden` - BrowserOnly check failed
|
|
- `429 Too Many Requests` - Rate limit exceeded
|
|
- `500 Internal Server Error` - Email send failure
|
|
|
|
**Security Features:**
|
|
1. **BrowserOnly Middleware:**
|
|
- Blocks curl, Postman, wget
|
|
- Requires User-Agent, Referer/Origin
|
|
- Requires HTMX or XMLHttpRequest headers
|
|
|
|
2. **Rate Limiting:**
|
|
- 5 submissions per hour per IP
|
|
- 1-hour window
|
|
- Automatic cleanup
|
|
|
|
3. **Validation:**
|
|
- Email header injection prevention
|
|
- Honeypot bot detection
|
|
- Timing-based bot detection
|
|
- HTML sanitization
|
|
|
|
### Protected PDF Endpoint
|
|
|
|
| Route | Method | Handler | Description | Security |
|
|
|-------|--------|---------|-------------|----------|
|
|
| `/export/pdf` | GET | `cvHandler.ExportPDF` | Generate and download PDF | OriginChecker + RateLimit(3/min) |
|
|
|
|
#### `/export/pdf` - PDF Export
|
|
|
|
**Purpose:** Generate and serve CV as PDF
|
|
|
|
**Handler:** `cvHandler.ExportPDF`
|
|
|
|
**Security Chain:**
|
|
```
|
|
OriginChecker → RateLimiter(3/min) → Handler
|
|
```
|
|
|
|
**Query Parameters:**
|
|
- `lang` - Language (en/es) - default: current preference
|
|
- `length` - CV length (short/full) - default: current preference
|
|
- `icons` - Show icons (true/false) - default: true
|
|
|
|
**Response Headers:**
|
|
```
|
|
Content-Type: application/pdf
|
|
Content-Disposition: attachment; filename="cv-jamr-2025-en.pdf"
|
|
Cache-Control: no-cache, no-store, must-revalidate
|
|
```
|
|
|
|
**Status Codes:**
|
|
- `200 OK` - PDF generated successfully
|
|
- `403 Forbidden` - Origin check failed or direct access blocked
|
|
- `429 Too Many Requests` - Rate limit exceeded (3/min)
|
|
- `500 Internal Server Error` - PDF generation failed
|
|
|
|
**OriginChecker:**
|
|
- Checks `Origin` or `Referer` header
|
|
- Allows: `juan.andres.morenorub.io`, `localhost`, `127.0.0.1`
|
|
- Blocks external site hotlinking
|
|
- In production: Blocks direct access (requires referer)
|
|
|
|
**RateLimiter:**
|
|
- Limit: 3 requests per minute per IP
|
|
- Window: 60 seconds
|
|
- Automatic entry cleanup
|
|
|
|
**Usage:**
|
|
```html
|
|
<a href="/export/pdf?lang=en&length=short" target="_blank">
|
|
Download Short CV (English)
|
|
</a>
|
|
```
|
|
|
|
### PDF Shortcut Routes
|
|
|
|
| Route | Method | Handler | Description |
|
|
|-------|--------|---------|-------------|
|
|
| `/cv-jamr-*` | GET | `cvHandler.DefaultCVShortcut` | Year-aware PDF shortcuts |
|
|
|
|
#### `/cv-jamr-*` - Default CV Shortcuts
|
|
|
|
**Purpose:** Friendly URLs for direct PDF access
|
|
|
|
**Handler:** `cvHandler.DefaultCVShortcut`
|
|
|
|
**Pattern:** `/cv-jamr-{year}-{lang}.pdf`
|
|
|
|
**Examples:**
|
|
- `/cv-jamr-2025-en.pdf` - English CV for 2025
|
|
- `/cv-jamr-2025-es.pdf` - Spanish CV for 2025
|
|
- `/cv-jamr-2024-en.pdf` - English CV for 2024
|
|
|
|
**Behavior:**
|
|
1. Parse year and language from URL
|
|
2. Redirect to `/export/pdf?lang={lang}&length=full`
|
|
3. Set appropriate filename in response
|
|
|
|
**Status Codes:**
|
|
- `200 OK` - PDF served successfully
|
|
- `302 Found` - Redirect to export endpoint
|
|
- `400 Bad Request` - Invalid URL format
|
|
|
|
### Static Files
|
|
|
|
| Route | Method | Handler | Description | Middleware |
|
|
|-------|--------|---------|-------------|------------|
|
|
| `/static/*` | GET | FileServer | CSS, JS, images, fonts | CacheControl |
|
|
|
|
#### `/static/*` - Static Assets
|
|
|
|
**Purpose:** Serve static files (CSS, JavaScript, images, fonts)
|
|
|
|
**Handler:** `http.FileServer(http.Dir("static"))`
|
|
|
|
**Directory Structure:**
|
|
```
|
|
static/
|
|
├── css/
|
|
│ ├── main.css
|
|
│ └── themes/
|
|
│ ├── light.css
|
|
│ └── dark.css
|
|
├── js/
|
|
│ ├── app.js
|
|
│ └── htmx.min.js
|
|
├── images/
|
|
│ ├── logo.png
|
|
│ └── avatar.jpg
|
|
├── fonts/
|
|
│ └── inter.woff2
|
|
└── icons/
|
|
└── sprites.svg
|
|
```
|
|
|
|
**Cache Headers:**
|
|
```
|
|
# Development
|
|
Cache-Control: public, max-age=3600 # 1 hour
|
|
|
|
# Production
|
|
Cache-Control: public, max-age=86400 # 1 day
|
|
```
|
|
|
|
**Middleware:** `CacheControl`
|
|
|
|
**Examples:**
|
|
```
|
|
/static/css/main.css
|
|
/static/js/htmx.min.js
|
|
/static/images/avatar.jpg
|
|
/static/fonts/inter.woff2
|
|
/static/icons/sprites.svg
|
|
```
|
|
|
|
## Middleware Stack
|
|
|
|
### Global Middleware Chain
|
|
|
|
Applied to **all routes** in this order:
|
|
|
|
```go
|
|
handler := middleware.Recovery(
|
|
middleware.Logger(
|
|
middleware.SecurityHeaders(
|
|
middleware.DynamicCacheControl(
|
|
middleware.PreferencesMiddleware(mux),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
```
|
|
|
|
#### 1. Recovery
|
|
|
|
**Purpose:** Panic recovery and graceful error handling
|
|
|
|
**File:** `internal/middleware/recovery.go`
|
|
|
|
**Behavior:**
|
|
- Catches panics in handlers
|
|
- Logs stack trace
|
|
- Returns 500 Internal Server Error
|
|
- Prevents server crash
|
|
|
|
**Error Response:**
|
|
```
|
|
500 Internal Server Error
|
|
Internal server error
|
|
```
|
|
|
|
---
|
|
|
|
#### 2. Logger
|
|
|
|
**Purpose:** Request logging for monitoring and debugging
|
|
|
|
**File:** `internal/middleware/logger.go`
|
|
|
|
**Logged Information:**
|
|
- HTTP method
|
|
- Request path
|
|
- Response status code
|
|
- Response time
|
|
- Client IP
|
|
|
|
**Log Format:**
|
|
```
|
|
2025-12-06 10:30:15 | 200 | 15.234ms | 192.168.1.100 | GET /cv
|
|
2025-12-06 10:30:16 | 429 | 0.523ms | 192.168.1.101 | POST /api/contact
|
|
```
|
|
|
|
---
|
|
|
|
#### 3. SecurityHeaders
|
|
|
|
**Purpose:** Comprehensive security headers
|
|
|
|
**File:** `internal/middleware/security.go`
|
|
|
|
**Headers Applied:**
|
|
|
|
| Header | Value | Purpose |
|
|
|--------|-------|---------|
|
|
| X-Frame-Options | SAMEORIGIN | Prevent clickjacking |
|
|
| X-Content-Type-Options | nosniff | Prevent MIME sniffing |
|
|
| X-XSS-Protection | 1; mode=block | XSS protection (legacy) |
|
|
| Referrer-Policy | strict-origin-when-cross-origin | Privacy |
|
|
| Permissions-Policy | geolocation=(), camera=(), ... | Disable unnecessary features |
|
|
| Content-Security-Policy | See CSP section below | XSS/injection prevention |
|
|
| Strict-Transport-Security | max-age=31536000 (prod only) | Force HTTPS |
|
|
|
|
**Content Security Policy (CSP):**
|
|
```
|
|
default-src 'self';
|
|
script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.jsdelivr.net https://esm.sh https://matomo.morenorub.io;
|
|
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
|
font-src 'self' https://fonts.gstatic.com;
|
|
img-src 'self' data: https:;
|
|
connect-src 'self' https://api.iconify.design https://matomo.morenorub.io;
|
|
frame-ancestors 'self';
|
|
base-uri 'self';
|
|
form-action 'self'
|
|
```
|
|
|
|
**HSTS (Production Only):**
|
|
```
|
|
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
|
|
```
|
|
|
|
---
|
|
|
|
#### 4. DynamicCacheControl
|
|
|
|
**Purpose:** Appropriate caching for dynamic HTML pages
|
|
|
|
**File:** `internal/middleware/security.go`
|
|
|
|
**Cache Headers:**
|
|
|
|
```bash
|
|
# Production
|
|
Cache-Control: public, max-age=300, must-revalidate # 5 minutes
|
|
|
|
# Development
|
|
Cache-Control: no-cache, no-store, must-revalidate
|
|
```
|
|
|
|
**Benefits:**
|
|
- Production: 5-minute cache reduces server load
|
|
- Development: No cache for easy testing
|
|
- `must-revalidate`: Ensures fresh content after expiry
|
|
|
|
---
|
|
|
|
#### 5. PreferencesMiddleware
|
|
|
|
**Purpose:** Parse and inject user preferences from cookies
|
|
|
|
**File:** `internal/middleware/preferences.go`
|
|
|
|
**Preferences:**
|
|
- Language (en/es)
|
|
- Theme (light/dark)
|
|
- CV length (short/full)
|
|
- Icons display (true/false)
|
|
|
|
**Behavior:**
|
|
1. Read preferences cookies
|
|
2. Parse values
|
|
3. Inject into request context
|
|
4. Available to handlers via context
|
|
|
|
**Cookie Names:**
|
|
```
|
|
cv_language=en
|
|
cv_theme=dark
|
|
cv_length=full
|
|
cv_icons=true
|
|
```
|
|
|
|
### Route-Specific Middleware
|
|
|
|
#### BrowserOnly
|
|
|
|
**Applied To:** `/api/contact`
|
|
|
|
**Purpose:** Block non-browser HTTP clients
|
|
|
|
**File:** `internal/middleware/browser_only.go`
|
|
|
|
**Checks:**
|
|
1. **User-Agent Validation:**
|
|
- Must be present
|
|
- Must not be curl, wget, Postman, etc.
|
|
|
|
2. **Referer/Origin Validation:**
|
|
- At least one must be present
|
|
- Prevents direct API calls
|
|
|
|
3. **Custom Header Validation:**
|
|
- `HX-Request: true` (HTMX), OR
|
|
- `X-Requested-With: XMLHttpRequest`, OR
|
|
- `X-Browser-Request: true`
|
|
|
|
**Blocked User-Agents:**
|
|
```go
|
|
curl, wget, postman, insomnia, httpie, python-requests,
|
|
python-urllib, java, okhttp, go-http-client, axios,
|
|
node-fetch, apache-httpclient, libwww-perl, php, ruby,
|
|
scrapy, bot, crawler, spider
|
|
```
|
|
|
|
**Error Response (403 Forbidden):**
|
|
```
|
|
Forbidden: Browser access only
|
|
```
|
|
|
|
**Security Benefit:** Prevents automated bot submissions and API abuse
|
|
|
|
---
|
|
|
|
#### OriginChecker
|
|
|
|
**Applied To:** `/export/pdf`
|
|
|
|
**Purpose:** Prevent external site hotlinking
|
|
|
|
**File:** `internal/middleware/security.go`
|
|
|
|
**Allowed Origins:**
|
|
- `juan.andres.morenorub.io` (production domain)
|
|
- `localhost` (development)
|
|
- `127.0.0.1` (development)
|
|
- Custom domains from `ALLOWED_ORIGINS` env var
|
|
|
|
**Validation:**
|
|
1. Check `Origin` header (CORS requests)
|
|
2. Check `Referer` header (direct requests)
|
|
3. In production: Require at least referer for PDF endpoint
|
|
|
|
**Error Response (403 Forbidden):**
|
|
```
|
|
Forbidden: External access not allowed
|
|
Forbidden: Direct access not allowed (production only)
|
|
```
|
|
|
|
**Environment Configuration:**
|
|
```bash
|
|
ALLOWED_ORIGINS="yourdomain.com,www.yourdomain.com"
|
|
```
|
|
|
|
---
|
|
|
|
#### RateLimiter
|
|
|
|
**Applied To:**
|
|
- `/api/contact` - 5 requests/hour
|
|
- `/export/pdf` - 3 requests/minute
|
|
|
|
**Purpose:** Prevent abuse and excessive resource usage
|
|
|
|
**File:** `internal/middleware/security.go`
|
|
|
|
**Implementation:**
|
|
|
|
```go
|
|
// Contact form rate limiter
|
|
contactRateLimiter := middleware.NewRateLimiter(5, 1*time.Hour)
|
|
|
|
// PDF export rate limiter
|
|
pdfRateLimiter := middleware.NewRateLimiter(3, 1*time.Minute)
|
|
```
|
|
|
|
**Features:**
|
|
- Per-IP tracking
|
|
- Configurable limit and window
|
|
- Automatic cleanup of expired entries
|
|
- Thread-safe (sync.RWMutex)
|
|
|
|
**Rate Limit Algorithm:**
|
|
```
|
|
1. Get client IP (X-Forwarded-For → X-Real-IP → RemoteAddr)
|
|
2. Check if IP has entry
|
|
3. If no entry or expired:
|
|
- Create new entry with count=1
|
|
- Set resetTime = now + window
|
|
- Allow request
|
|
4. If entry exists and not expired:
|
|
- If count >= limit: Deny
|
|
- Else: Increment count, Allow
|
|
```
|
|
|
|
**Error Response (429 Too Many Requests):**
|
|
```
|
|
HTTP/1.1 429 Too Many Requests
|
|
Retry-After: 60
|
|
|
|
Rate limit exceeded. Please try again later.
|
|
```
|
|
|
|
**HTMX Error Response:**
|
|
```html
|
|
<div class="alert alert-error">
|
|
<h3>Too Many Requests</h3>
|
|
<p>You've submitted too many contact forms. Please wait an hour before trying again.</p>
|
|
</div>
|
|
```
|
|
|
|
**Cleanup:**
|
|
- Runs every 1 minute (general) or 10 minutes (contact)
|
|
- Removes expired IP entries
|
|
- Prevents memory leak
|
|
|
|
---
|
|
|
|
#### CacheControl
|
|
|
|
**Applied To:** `/static/*`
|
|
|
|
**Purpose:** Aggressive caching for static assets
|
|
|
|
**File:** `internal/middleware/security.go`
|
|
|
|
**Cache Headers:**
|
|
|
|
```bash
|
|
# Development
|
|
Cache-Control: public, max-age=3600 # 1 hour
|
|
|
|
# Production
|
|
Cache-Control: public, max-age=86400 # 1 day
|
|
```
|
|
|
|
**Benefits:**
|
|
- Reduces bandwidth
|
|
- Improves page load speed
|
|
- Offloads server processing
|
|
|
|
**Cache Busting:**
|
|
```html
|
|
<!-- Version in filename for cache busting -->
|
|
<link rel="stylesheet" href="/static/css/main.css?v=1.2.3">
|
|
```
|
|
|
|
## Security Features
|
|
|
|
### 1. HTTPS Enforcement (Production)
|
|
|
|
```go
|
|
if os.Getenv("GO_ENV") == "production" {
|
|
w.Header().Set("Strict-Transport-Security",
|
|
"max-age=31536000; includeSubDomains; preload")
|
|
}
|
|
```
|
|
|
|
**Effect:** Forces HTTPS for 1 year, includes subdomains
|
|
|
|
### 2. Content Security Policy
|
|
|
|
Prevents XSS and injection attacks by whitelisting allowed sources:
|
|
|
|
```
|
|
script-src 'self' https://unpkg.com https://cdn.jsdelivr.net
|
|
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com
|
|
img-src 'self' data: https:
|
|
```
|
|
|
|
### 3. Multi-Layer Bot Protection
|
|
|
|
**Contact Form:**
|
|
1. BrowserOnly middleware
|
|
2. Honeypot field (`website` must be empty)
|
|
3. Timing validation (2s-24h)
|
|
4. Rate limiting (5/hour)
|
|
|
|
**PDF Export:**
|
|
1. OriginChecker (prevents hotlinking)
|
|
2. Rate limiting (3/minute)
|
|
|
|
### 4. Email Header Injection Prevention
|
|
|
|
```go
|
|
// Validation checks for newlines and email headers
|
|
if ContainsEmailInjection(req.Subject) {
|
|
return ValidationError{Field: "subject", Message: "Invalid characters"}
|
|
}
|
|
```
|
|
|
|
**Blocked Patterns:**
|
|
- `\r`, `\n` characters
|
|
- `bcc:`, `cc:`, `to:`, `from:`
|
|
- `content-type:`, `mime-version:`
|
|
|
|
### 5. XSS Prevention
|
|
|
|
**Template Auto-Escaping:**
|
|
```html
|
|
<!-- User input: <script>alert('XSS')</script> -->
|
|
<p>{{.UserInput}}</p>
|
|
<!-- Output: <script>alert('XSS')</script> -->
|
|
```
|
|
|
|
**Validation Sanitization:**
|
|
```go
|
|
Message string `validate:"required,trim,max=5000,sanitize"`
|
|
// Result: HTML-escaped, newlines removed
|
|
```
|
|
|
|
## Error Responses
|
|
|
|
### Standard Error Format
|
|
|
|
```json
|
|
{
|
|
"success": false,
|
|
"error": "Error message",
|
|
"errors": [
|
|
{
|
|
"field": "email",
|
|
"tag": "email",
|
|
"message": "Invalid email address format"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### HTTP Status Codes
|
|
|
|
| Code | Meaning | Usage |
|
|
|------|---------|-------|
|
|
| 200 | OK | Successful request |
|
|
| 302 | Found | PDF shortcut redirect |
|
|
| 400 | Bad Request | Validation error |
|
|
| 403 | Forbidden | BrowserOnly or OriginChecker failed |
|
|
| 404 | Not Found | Route not found |
|
|
| 429 | Too Many Requests | Rate limit exceeded |
|
|
| 500 | Internal Server Error | Server error, template error |
|
|
| 503 | Service Unavailable | Health check failed |
|
|
|
|
## Configuration
|
|
|
|
### Environment Variables
|
|
|
|
```bash
|
|
# Application environment
|
|
GO_ENV=production # or "development"
|
|
|
|
# Allowed origins for PDF export
|
|
ALLOWED_ORIGINS="juan.andres.morenorub.io,www.juan.andres.morenorub.io"
|
|
|
|
# Template hot reload (development)
|
|
TEMPLATE_HOT_RELOAD=true
|
|
|
|
# Server configuration
|
|
PORT=8080
|
|
HOST=0.0.0.0
|
|
```
|
|
|
|
### Route Priority
|
|
|
|
Routes are registered in order of specificity to avoid conflicts:
|
|
|
|
```go
|
|
// 1. Specific patterns first
|
|
mux.HandleFunc("/cv-jamr-", cvHandler.DefaultCVShortcut)
|
|
mux.HandleFunc("/api/cmd-k", cvHandler.CmdKData)
|
|
|
|
// 2. Protected endpoints
|
|
mux.Handle("/api/contact", protectedContactHandler)
|
|
mux.Handle("/export/pdf", protectedPDFHandler)
|
|
|
|
// 3. General routes
|
|
mux.HandleFunc("/", cvHandler.Home)
|
|
mux.HandleFunc("/cv", cvHandler.CVContent)
|
|
|
|
// 4. Static files (catch-all)
|
|
mux.Handle("/static/", middleware.CacheControl(staticHandler))
|
|
```
|
|
|
|
## Performance Considerations
|
|
|
|
### 1. Middleware Order Optimization
|
|
|
|
```go
|
|
// Fast-fail first (Recovery catches panics immediately)
|
|
Recovery → Logger → SecurityHeaders → DynamicCache → Preferences → Mux
|
|
```
|
|
|
|
### 2. Rate Limiter Efficiency
|
|
|
|
```go
|
|
// RWMutex for concurrent reads
|
|
type RateLimiter struct {
|
|
mu sync.RWMutex // Read-heavy workload
|
|
clients map[string]*rateLimitEntry
|
|
}
|
|
```
|
|
|
|
**Performance:**
|
|
- Allow check: ~100-200 ns
|
|
- Memory per IP: ~48 bytes
|
|
- Cleanup overhead: Negligible (1/min)
|
|
|
|
### 3. Template Caching
|
|
|
|
Production mode (HotReload=false):
|
|
- Templates loaded once at startup
|
|
- Zero reload overhead
|
|
- Thread-safe concurrent rendering
|
|
|
|
### 4. Static File Serving
|
|
|
|
```go
|
|
// Native Go file server with proper cache headers
|
|
http.FileServer(http.Dir("static"))
|
|
```
|
|
|
|
**Benefits:**
|
|
- Efficient sendfile() syscall
|
|
- Range request support
|
|
- ETag generation
|
|
- Gzip compression (if configured)
|
|
|
|
## Monitoring and Observability
|
|
|
|
### Request Logging
|
|
|
|
```
|
|
2025-12-06 10:30:15 | 200 | 15.234ms | 192.168.1.100 | GET /cv
|
|
2025-12-06 10:30:16 | 429 | 0.523ms | 192.168.1.101 | POST /api/contact
|
|
2025-12-06 10:30:17 | 200 | 125.678ms| 192.168.1.102 | GET /export/pdf
|
|
```
|
|
|
|
**Logged Fields:**
|
|
- Timestamp
|
|
- Status code
|
|
- Response time
|
|
- Client IP
|
|
- Method + Path
|
|
|
|
### Health Check Endpoint
|
|
|
|
```bash
|
|
curl https://juan.andres.morenorub.io/health
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": "healthy",
|
|
"timestamp": "2025-12-06T10:30:00Z",
|
|
"version": "1.0.0"
|
|
}
|
|
```
|
|
|
|
**Use Cases:**
|
|
- Load balancer health checks
|
|
- Uptime monitoring
|
|
- Deployment verification
|
|
|
|
### Rate Limiter Statistics
|
|
|
|
```go
|
|
// Available for monitoring dashboards
|
|
func (rl *RateLimiter) GetStats() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"total_clients": len(rl.clients),
|
|
"limit": rl.limit,
|
|
"window": rl.window.String(),
|
|
}
|
|
}
|
|
```
|
|
|
|
## Testing Routes
|
|
|
|
### Manual Testing
|
|
|
|
```bash
|
|
# Home page
|
|
curl https://juan.andres.morenorub.io/
|
|
|
|
# Plain text CV
|
|
curl https://juan.andres.morenorub.io/text
|
|
|
|
# Health check
|
|
curl https://juan.andres.morenorub.io/health
|
|
|
|
# PDF export (will be blocked - needs browser)
|
|
curl https://juan.andres.morenorub.io/export/pdf
|
|
|
|
# Contact form (will be blocked - needs browser)
|
|
curl -X POST https://juan.andres.morenorub.io/api/contact \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"name":"Test","email":"test@example.com",...}'
|
|
```
|
|
|
|
### Automated Testing
|
|
|
|
```go
|
|
// Test route handlers
|
|
func TestHomeHandler(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Expected 200, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// Test middleware
|
|
func TestBrowserOnly(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/contact", nil)
|
|
req.Header.Set("User-Agent", "curl/7.68.0")
|
|
|
|
w := httptest.NewRecorder()
|
|
middleware.BrowserOnly(mockHandler).ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("Expected 403, got %d", w.Code)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Quick Reference
|
|
|
|
### Route Overview
|
|
|
|
```
|
|
Public:
|
|
/ → Home page
|
|
/cv → CV content partial
|
|
/text → Plain text CV
|
|
/health → Health check
|
|
|
|
HTMX:
|
|
/switch-language → Toggle EN/ES
|
|
/toggle/length → Toggle short/full
|
|
/toggle/icons → Toggle icons
|
|
/toggle/theme → Toggle light/dark
|
|
|
|
API:
|
|
/api/cmd-k → Command palette data
|
|
/api/contact → Contact form (protected)
|
|
|
|
Protected:
|
|
/export/pdf → PDF generation (rate limited)
|
|
/cv-jamr-* → PDF shortcuts
|
|
|
|
Static:
|
|
/static/* → CSS, JS, images, fonts
|
|
```
|
|
|
|
### Middleware Chains
|
|
|
|
```
|
|
Global (all routes):
|
|
Recovery → Logger → SecurityHeaders → DynamicCache → Preferences
|
|
|
|
Contact Form:
|
|
+ BrowserOnly → RateLimiter(5/hour)
|
|
|
|
PDF Export:
|
|
+ OriginChecker → RateLimiter(3/min)
|
|
|
|
Static Files:
|
|
+ CacheControl
|
|
```
|
|
|
|
### Security Headers
|
|
|
|
```
|
|
X-Frame-Options: SAMEORIGIN
|
|
X-Content-Type-Options: nosniff
|
|
X-XSS-Protection: 1; mode=block
|
|
Referrer-Policy: strict-origin-when-cross-origin
|
|
Content-Security-Policy: (comprehensive policy)
|
|
Strict-Transport-Security: max-age=31536000 (production only)
|
|
```
|
|
|
|
## Related Files
|
|
|
|
- `internal/routes/routes.go` - Route setup and middleware chain
|
|
- `internal/middleware/security.go` - Security middleware
|
|
- `internal/middleware/browser_only.go` - BrowserOnly middleware
|
|
- `internal/middleware/contact_rate_limit.go` - Contact rate limiting
|
|
- `internal/middleware/logger.go` - Request logging
|
|
- `internal/middleware/recovery.go` - Panic recovery
|
|
- `internal/middleware/preferences.go` - User preferences
|
|
- `internal/handlers/cv.go` - CV handlers
|
|
- `internal/handlers/health.go` - Health check handler
|
|
- `internal/handlers/cv_contact.go` - Contact form handler
|
|
- `internal/handlers/cv_pdf.go` - PDF export handler
|
|
|
|
## See Also
|
|
|
|
- [Validation System Documentation](go-validation-system.md)
|
|
- [Template System Documentation](go-template-system.md)
|
|
- [Go net/http Package](https://pkg.go.dev/net/http)
|