refactor: use 'c' alias for constants package
- Update all imports from 'constants' to 'c' for brevity - Replace all 'constants.' references with 'c.' - Fix remaining hardcoded content-type headers in httputil - Fix remaining hardcoded User-Agent and Accept headers - Rename CSRF receiver from 'c' to 'csrf' to avoid conflict - Add ContentTypePlainSimple constant for Accept header matching - Fix JSONCached to use proper integer formatting
This commit is contained in:
+1162
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config holds all application configuration
|
// Config holds all application configuration
|
||||||
@@ -50,7 +50,7 @@ type EmailConfig struct {
|
|||||||
func Load() *Config {
|
func Load() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Port: getEnv(constants.EnvVarPort, constants.DefaultPort),
|
Port: getEnv(c.EnvVarPort, c.DefaultPort),
|
||||||
Host: getEnv("HOST", "localhost"),
|
Host: getEnv("HOST", "localhost"),
|
||||||
ReadTimeout: getEnvAsInt("READ_TIMEOUT", 15),
|
ReadTimeout: getEnvAsInt("READ_TIMEOUT", 15),
|
||||||
WriteTimeout: getEnvAsInt("WRITE_TIMEOUT", 15),
|
WriteTimeout: getEnvAsInt("WRITE_TIMEOUT", 15),
|
||||||
@@ -105,6 +105,6 @@ func getEnvAsBool(key string, defaultValue bool) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isDevelopment() bool {
|
func isDevelopment() bool {
|
||||||
env := getEnv(constants.EnvVarGOEnv, constants.EnvDevelopment)
|
env := getEnv(c.EnvVarGOEnv, c.EnvDevelopment)
|
||||||
return env == constants.EnvDevelopment || env == "dev"
|
return env == c.EnvDevelopment || env == "dev"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const (
|
|||||||
ContentTypeHTML = "text/html; charset=utf-8"
|
ContentTypeHTML = "text/html; charset=utf-8"
|
||||||
ContentTypeHTMLFragment = "text/html" // For HTMX fragments
|
ContentTypeHTMLFragment = "text/html" // For HTMX fragments
|
||||||
ContentTypePlainText = "text/plain; charset=utf-8"
|
ContentTypePlainText = "text/plain; charset=utf-8"
|
||||||
|
ContentTypePlainSimple = "text/plain" // For Accept header matching
|
||||||
ContentTypePDF = "application/pdf"
|
ContentTypePDF = "application/pdf"
|
||||||
ContentTypeFormURLEnc = "application/x-www-form-urlencoded"
|
ContentTypeFormURLEnc = "application/x-www-form-urlencoded"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
"github.com/juanatsap/cv-site/internal/services"
|
"github.com/juanatsap/cv-site/internal/services"
|
||||||
"github.com/juanatsap/cv-site/internal/templates"
|
"github.com/juanatsap/cv-site/internal/templates"
|
||||||
)
|
)
|
||||||
@@ -143,7 +143,7 @@ func (h *ContactHandler) Submit(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// renderSuccess renders the success partial
|
// renderSuccess renders the success partial
|
||||||
func (h *ContactHandler) renderSuccess(w http.ResponseWriter, r *http.Request) {
|
func (h *ContactHandler) renderSuccess(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypeHTML)
|
w.Header().Set(c.HeaderContentType, c.ContentTypeHTML)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
// Fallback HTML for when templates aren't available (e.g., in tests)
|
// Fallback HTML for when templates aren't available (e.g., in tests)
|
||||||
@@ -173,7 +173,7 @@ func (h *ContactHandler) renderSuccess(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// renderError renders the error partial
|
// renderError renders the error partial
|
||||||
func (h *ContactHandler) renderError(w http.ResponseWriter, r *http.Request, message string) {
|
func (h *ContactHandler) renderError(w http.ResponseWriter, r *http.Request, message string) {
|
||||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypeHTML)
|
w.Header().Set(c.HeaderContentType, c.ContentTypeHTML)
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
|
||||||
// Fallback HTML for when templates aren't available (e.g., in tests)
|
// Fallback HTML for when templates aren't available (e.g., in tests)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
"github.com/juanatsap/cv-site/internal/httputil"
|
"github.com/juanatsap/cv-site/internal/httputil"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -89,8 +89,8 @@ func (h *CVHandler) CmdKData(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set headers and encode response
|
// Set headers and encode response
|
||||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
|
w.Header().Set(c.HeaderContentType, c.ContentTypeJSON)
|
||||||
w.Header().Set(constants.HeaderCacheControl, constants.CachePublic1Hour)
|
w.Header().Set(c.HeaderCacheControl, c.CachePublic1Hour)
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||||
log.Printf("ERROR encoding CMD+K response: %v", err)
|
log.Printf("ERROR encoding CMD+K response: %v", err)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
"github.com/juanatsap/cv-site/internal/httputil"
|
"github.com/juanatsap/cv-site/internal/httputil"
|
||||||
"github.com/juanatsap/cv-site/internal/services"
|
"github.com/juanatsap/cv-site/internal/services"
|
||||||
)
|
)
|
||||||
@@ -173,7 +173,7 @@ func (h *CVHandler) renderContactSuccess(w http.ResponseWriter, r *http.Request,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render the success template
|
// Render the success template
|
||||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypeHTML)
|
w.Header().Set(c.HeaderContentType, c.ContentTypeHTML)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
tmpl, err := h.templates.Render("contact-success")
|
tmpl, err := h.templates.Render("contact-success")
|
||||||
@@ -224,7 +224,7 @@ func (h *CVHandler) renderContactError(w http.ResponseWriter, r *http.Request, e
|
|||||||
// Render the error template
|
// Render the error template
|
||||||
// Return 200 OK with error content - HTMX 1.9.x logs console.error for non-2xx responses
|
// Return 200 OK with error content - HTMX 1.9.x logs console.error for non-2xx responses
|
||||||
// Validation errors are expected form feedback, not system errors
|
// Validation errors are expected form feedback, not system errors
|
||||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypeHTML)
|
w.Header().Set(c.HeaderContentType, c.ContentTypeHTML)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
tmpl, err := h.templates.Render("contact-error")
|
tmpl, err := h.templates.Render("contact-error")
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
cvmodel "github.com/juanatsap/cv-site/internal/models/cv"
|
cvmodel "github.com/juanatsap/cv-site/internal/models/cv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -343,7 +343,7 @@ func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, er
|
|||||||
|
|
||||||
// Check if production mode AND CSS bundle exists
|
// Check if production mode AND CSS bundle exists
|
||||||
// This ensures graceful fallback to modular CSS if bundle not built
|
// This ensures graceful fallback to modular CSS if bundle not built
|
||||||
isProduction := os.Getenv(constants.EnvVarGOEnv) == constants.EnvProduction
|
isProduction := os.Getenv(c.EnvVarGOEnv) == c.EnvProduction
|
||||||
if isProduction {
|
if isProduction {
|
||||||
bundlePath := filepath.Join("static", "dist", "bundle.min.css")
|
bundlePath := filepath.Join("static", "dist", "bundle.min.css")
|
||||||
if _, err := os.Stat(bundlePath); os.IsNotExist(err) {
|
if _, err := os.Stat(bundlePath); os.IsNotExist(err) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
"github.com/juanatsap/cv-site/internal/httputil"
|
"github.com/juanatsap/cv-site/internal/httputil"
|
||||||
"github.com/juanatsap/cv-site/internal/middleware"
|
"github.com/juanatsap/cv-site/internal/middleware"
|
||||||
)
|
)
|
||||||
@@ -25,13 +25,13 @@ func (h *CVHandler) ToggleLength(w http.ResponseWriter, r *http.Request) {
|
|||||||
currentLength := prefs.CVLength
|
currentLength := prefs.CVLength
|
||||||
|
|
||||||
// Toggle state
|
// Toggle state
|
||||||
newLength := constants.CVLengthLong
|
newLength := c.CVLengthLong
|
||||||
if currentLength == constants.CVLengthLong {
|
if currentLength == c.CVLengthLong {
|
||||||
newLength = constants.CVLengthShort
|
newLength = c.CVLengthShort
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save new state
|
// Save new state
|
||||||
middleware.SetPreferenceCookie(w, constants.CookieCVLength, newLength)
|
middleware.SetPreferenceCookie(w, c.CookieCVLength, newLength)
|
||||||
|
|
||||||
// Return 204 No Content - frontend uses hx-swap="none" so response body is ignored
|
// Return 204 No Content - frontend uses hx-swap="none" so response body is ignored
|
||||||
// The cookie is set and hyperscript handles the UI state toggle
|
// The cookie is set and hyperscript handles the UI state toggle
|
||||||
@@ -49,13 +49,13 @@ func (h *CVHandler) ToggleIcons(w http.ResponseWriter, r *http.Request) {
|
|||||||
currentIcons := prefs.CVIcons
|
currentIcons := prefs.CVIcons
|
||||||
|
|
||||||
// Toggle state
|
// Toggle state
|
||||||
newIcons := constants.CVIconsHide
|
newIcons := c.CVIconsHide
|
||||||
if currentIcons == constants.CVIconsHide {
|
if currentIcons == c.CVIconsHide {
|
||||||
newIcons = constants.CVIconsShow
|
newIcons = c.CVIconsShow
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save new state
|
// Save new state
|
||||||
middleware.SetPreferenceCookie(w, constants.CookieCVIcons, newIcons)
|
middleware.SetPreferenceCookie(w, c.CookieCVIcons, newIcons)
|
||||||
|
|
||||||
// Return 204 No Content - frontend uses hx-swap="none" so response body is ignored
|
// Return 204 No Content - frontend uses hx-swap="none" so response body is ignored
|
||||||
// The cookie is set and hyperscript handles the UI state toggle
|
// The cookie is set and hyperscript handles the UI state toggle
|
||||||
@@ -73,7 +73,7 @@ func (h *CVHandler) SwitchLanguage(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save language preference
|
// Save language preference
|
||||||
middleware.SetPreferenceCookie(w, constants.CookieCVLanguage, lang)
|
middleware.SetPreferenceCookie(w, c.CookieCVLanguage, lang)
|
||||||
|
|
||||||
// Prepare template data
|
// Prepare template data
|
||||||
data, err := h.prepareTemplateData(lang)
|
data, err := h.prepareTemplateData(lang)
|
||||||
@@ -86,13 +86,13 @@ func (h *CVHandler) SwitchLanguage(w http.ResponseWriter, r *http.Request) {
|
|||||||
prefs := middleware.GetPreferences(r)
|
prefs := middleware.GetPreferences(r)
|
||||||
|
|
||||||
// Add preferences to data
|
// Add preferences to data
|
||||||
if prefs.CVLength == constants.CVLengthLong {
|
if prefs.CVLength == c.CVLengthLong {
|
||||||
data["CVLengthClass"] = "cv-long"
|
data["CVLengthClass"] = "cv-long"
|
||||||
} else {
|
} else {
|
||||||
data["CVLengthClass"] = "cv-short"
|
data["CVLengthClass"] = "cv-short"
|
||||||
}
|
}
|
||||||
data["ShowIcons"] = (prefs.CVIcons == constants.CVIconsShow)
|
data["ShowIcons"] = (prefs.CVIcons == c.CVIconsShow)
|
||||||
data["ThemeClean"] = (prefs.CVTheme == constants.CVThemeClean)
|
data["ThemeClean"] = (prefs.CVTheme == c.CVThemeClean)
|
||||||
|
|
||||||
// Render language-switch template with out-of-band swaps
|
// Render language-switch template with out-of-band swaps
|
||||||
tmpl, err := h.templates.Render("language-switch.html")
|
tmpl, err := h.templates.Render("language-switch.html")
|
||||||
@@ -101,7 +101,7 @@ func (h *CVHandler) SwitchLanguage(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypeHTML)
|
w.Header().Set(c.HeaderContentType, c.ContentTypeHTML)
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
HandleError(w, r, TemplateError(err, "language-switch.html"))
|
HandleError(w, r, TemplateError(err, "language-switch.html"))
|
||||||
return
|
return
|
||||||
@@ -119,13 +119,13 @@ func (h *CVHandler) ToggleTheme(w http.ResponseWriter, r *http.Request) {
|
|||||||
currentTheme := prefs.CVTheme
|
currentTheme := prefs.CVTheme
|
||||||
|
|
||||||
// Toggle state
|
// Toggle state
|
||||||
newTheme := constants.CVThemeClean
|
newTheme := c.CVThemeClean
|
||||||
if currentTheme == constants.CVThemeClean {
|
if currentTheme == c.CVThemeClean {
|
||||||
newTheme = constants.CVThemeDefault
|
newTheme = c.CVThemeDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save new state
|
// Save new state
|
||||||
middleware.SetPreferenceCookie(w, constants.CookieCVTheme, newTheme)
|
middleware.SetPreferenceCookie(w, c.CookieCVTheme, newTheme)
|
||||||
|
|
||||||
// Return 204 No Content - frontend uses hx-swap="none" so response body is ignored
|
// Return 204 No Content - frontend uses hx-swap="none" so response body is ignored
|
||||||
// The cookie is set and hyperscript handles the UI state toggle
|
// The cookie is set and hyperscript handles the UI state toggle
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
"github.com/juanatsap/cv-site/internal/httputil"
|
"github.com/juanatsap/cv-site/internal/httputil"
|
||||||
"github.com/juanatsap/cv-site/internal/middleware"
|
"github.com/juanatsap/cv-site/internal/middleware"
|
||||||
"github.com/juanatsap/cv-site/internal/pdf"
|
"github.com/juanatsap/cv-site/internal/pdf"
|
||||||
@@ -70,7 +70,7 @@ func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypeHTML)
|
w.Header().Set(c.HeaderContentType, c.ContentTypeHTML)
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
HandleError(w, r, TemplateError(err, "index.html"))
|
HandleError(w, r, TemplateError(err, "index.html"))
|
||||||
return
|
return
|
||||||
@@ -99,7 +99,7 @@ func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypeHTML)
|
w.Header().Set(c.HeaderContentType, c.ContentTypeHTML)
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
HandleError(w, r, TemplateError(err, "cv-content.html"))
|
HandleError(w, r, TemplateError(err, "cv-content.html"))
|
||||||
return
|
return
|
||||||
@@ -127,7 +127,7 @@ func (h *CVHandler) DefaultCVShortcut(w http.ResponseWriter, r *http.Request) {
|
|||||||
lang := strings.TrimSuffix(langWithExt, ".pdf")
|
lang := strings.TrimSuffix(langWithExt, ".pdf")
|
||||||
|
|
||||||
// Validate language
|
// Validate language
|
||||||
if !constants.SupportedLanguages[lang] {
|
if !c.SupportedLanguages[lang] {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -147,11 +147,11 @@ func (h *CVHandler) DefaultCVShortcut(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Prepare cookies for PDF generation (short, with_skills, light mode)
|
// Prepare cookies for PDF generation (short, with_skills, light mode)
|
||||||
cookies := map[string]string{
|
cookies := map[string]string{
|
||||||
constants.CookieCVLength: constants.CVLengthShort,
|
c.CookieCVLength: c.CVLengthShort,
|
||||||
constants.CookieCVIcons: constants.CVIconsShow,
|
c.CookieCVIcons: c.CVIconsShow,
|
||||||
constants.CookieCVLanguage: lang,
|
c.CookieCVLanguage: lang,
|
||||||
constants.CookieCVTheme: constants.CVThemeDefault, // with_skills = default theme
|
c.CookieCVTheme: c.CVThemeDefault, // with_skills = default theme
|
||||||
constants.CookieColorTheme: constants.ColorThemeLight, // Always light for PDFs
|
c.CookieColorTheme: c.ColorThemeLight, // Always light for PDFs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct URL for PDF generation
|
// Construct URL for PDF generation
|
||||||
@@ -170,9 +170,9 @@ func (h *CVHandler) DefaultCVShortcut(w http.ResponseWriter, r *http.Request) {
|
|||||||
filename := filepath.Base(path) // cv-jamr-2025-en.pdf
|
filename := filepath.Base(path) // cv-jamr-2025-en.pdf
|
||||||
|
|
||||||
// Set response headers with shortcut filename
|
// Set response headers with shortcut filename
|
||||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypePDF)
|
w.Header().Set(c.HeaderContentType, c.ContentTypePDF)
|
||||||
w.Header().Set(constants.HeaderContentDisposition, fmt.Sprintf("attachment; filename=%s", filename))
|
w.Header().Set(c.HeaderContentDisposition, fmt.Sprintf("attachment; filename=%s", filename))
|
||||||
w.Header().Set(constants.HeaderContentLength, fmt.Sprintf("%d", len(pdfData)))
|
w.Header().Set(c.HeaderContentLength, fmt.Sprintf("%d", len(pdfData)))
|
||||||
|
|
||||||
// Write PDF data
|
// Write PDF data
|
||||||
if _, err := w.Write(pdfData); err != nil {
|
if _, err := w.Write(pdfData); err != nil {
|
||||||
|
|||||||
+11
-11
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
cvmodel "github.com/juanatsap/cv-site/internal/models/cv"
|
cvmodel "github.com/juanatsap/cv-site/internal/models/cv"
|
||||||
"github.com/juanatsap/cv-site/internal/pdf"
|
"github.com/juanatsap/cv-site/internal/pdf"
|
||||||
)
|
)
|
||||||
@@ -38,21 +38,21 @@ func (h *CVHandler) ExportPDF(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Prepare cookies to set preferences
|
// Prepare cookies to set preferences
|
||||||
cookies := map[string]string{
|
cookies := map[string]string{
|
||||||
constants.CookieCVLength: req.Length,
|
c.CookieCVLength: req.Length,
|
||||||
constants.CookieCVIcons: req.Icons,
|
c.CookieCVIcons: req.Icons,
|
||||||
constants.CookieCVLanguage: req.Lang,
|
c.CookieCVLanguage: req.Lang,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set theme cookie based on version parameter
|
// Set theme cookie based on version parameter
|
||||||
if req.Version == constants.CVThemeClean {
|
if req.Version == c.CVThemeClean {
|
||||||
cookies[constants.CookieCVTheme] = constants.CVThemeClean
|
cookies[c.CookieCVTheme] = c.CVThemeClean
|
||||||
} else {
|
} else {
|
||||||
cookies[constants.CookieCVTheme] = constants.CVThemeDefault
|
cookies[c.CookieCVTheme] = c.CVThemeDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRITICAL: ALWAYS force light mode for PDF generation (print-friendly)
|
// CRITICAL: ALWAYS force light mode for PDF generation (print-friendly)
|
||||||
// This ensures PDFs are NEVER generated in dark mode, regardless of user's preference
|
// This ensures PDFs are NEVER generated in dark mode, regardless of user's preference
|
||||||
cookies[constants.CookieColorTheme] = constants.ColorThemeLight
|
cookies[c.CookieColorTheme] = c.ColorThemeLight
|
||||||
|
|
||||||
// Construct URL for PDF generation (navigate to home page)
|
// Construct URL for PDF generation (navigate to home page)
|
||||||
targetURL := fmt.Sprintf("http://%s/?lang=%s", h.serverAddr, req.Lang)
|
targetURL := fmt.Sprintf("http://%s/?lang=%s", h.serverAddr, req.Lang)
|
||||||
@@ -111,9 +111,9 @@ func (h *CVHandler) ExportPDF(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set response headers
|
// Set response headers
|
||||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypePDF)
|
w.Header().Set(c.HeaderContentType, c.ContentTypePDF)
|
||||||
w.Header().Set(constants.HeaderContentDisposition, fmt.Sprintf("attachment; filename=%s", filename))
|
w.Header().Set(c.HeaderContentDisposition, fmt.Sprintf("attachment; filename=%s", filename))
|
||||||
w.Header().Set(constants.HeaderContentLength, fmt.Sprintf("%d", len(pdfData)))
|
w.Header().Set(c.HeaderContentLength, fmt.Sprintf("%d", len(pdfData)))
|
||||||
|
|
||||||
// Write PDF data
|
// Write PDF data
|
||||||
if _, err := w.Write(pdfData); err != nil {
|
if _, err := w.Write(pdfData); err != nil {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
"github.com/juanatsap/cv-site/internal/httputil"
|
"github.com/juanatsap/cv-site/internal/httputil"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ var textBrowsers = []string{
|
|||||||
|
|
||||||
// isTextBrowser detects if the request comes from a text-based browser or CLI tool
|
// isTextBrowser detects if the request comes from a text-based browser or CLI tool
|
||||||
func isTextBrowser(r *http.Request) bool {
|
func isTextBrowser(r *http.Request) bool {
|
||||||
ua := strings.ToLower(r.Header.Get("User-Agent"))
|
ua := strings.ToLower(r.Header.Get(c.HeaderUserAgent))
|
||||||
|
|
||||||
// Check for known text browsers
|
// Check for known text browsers
|
||||||
for _, browser := range textBrowsers {
|
for _, browser := range textBrowsers {
|
||||||
@@ -46,8 +46,8 @@ func isTextBrowser(r *http.Request) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check Accept header - if client prefers text/plain
|
// Check Accept header - if client prefers text/plain
|
||||||
accept := r.Header.Get("Accept")
|
accept := r.Header.Get(c.HeaderAccept)
|
||||||
return strings.HasPrefix(accept, "text/plain")
|
return strings.HasPrefix(accept, c.ContentTypePlainSimple)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
@@ -150,8 +150,8 @@ func (h *CVHandler) PlainText(w http.ResponseWriter, r *http.Request) {
|
|||||||
text := cleanPlainText(buf.String())
|
text := cleanPlainText(buf.String())
|
||||||
|
|
||||||
// Set response headers
|
// Set response headers
|
||||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypePlainText)
|
w.Header().Set(c.HeaderContentType, c.ContentTypePlainText)
|
||||||
w.Header().Set(constants.HeaderXContentTypeOpts, constants.NoSniff)
|
w.Header().Set(c.HeaderXContentTypeOpts, c.NoSniff)
|
||||||
|
|
||||||
// Check if download is requested
|
// Check if download is requested
|
||||||
if r.URL.Query().Get("download") == "true" {
|
if r.URL.Query().Get("download") == "true" {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrorResponse represents a structured error response
|
// ErrorResponse represents a structured error response
|
||||||
@@ -64,12 +64,12 @@ func HandleError(w http.ResponseWriter, r *http.Request, err error) {
|
|||||||
|
|
||||||
// Determine response based on Accept header
|
// Determine response based on Accept header
|
||||||
accept := r.Header.Get("Accept")
|
accept := r.Header.Get("Accept")
|
||||||
isJSON := accept == constants.ContentTypeJSON
|
isJSON := accept == c.ContentTypeJSON
|
||||||
isHTMX := r.Header.Get(constants.HeaderHXRequest) != ""
|
isHTMX := r.Header.Get(c.HeaderHXRequest) != ""
|
||||||
|
|
||||||
if isJSON {
|
if isJSON {
|
||||||
// JSON response
|
// JSON response
|
||||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
|
w.Header().Set(c.HeaderContentType, c.ContentTypeJSON)
|
||||||
w.WriteHeader(appErr.StatusCode)
|
w.WriteHeader(appErr.StatusCode)
|
||||||
|
|
||||||
response := ErrorResponse{
|
response := ErrorResponse{
|
||||||
@@ -90,7 +90,7 @@ func HandleError(w http.ResponseWriter, r *http.Request, err error) {
|
|||||||
|
|
||||||
if isHTMX {
|
if isHTMX {
|
||||||
// HTMX response - return simple HTML fragment
|
// HTMX response - return simple HTML fragment
|
||||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypeHTMLFragment)
|
w.Header().Set(c.HeaderContentType, c.ContentTypeHTMLFragment)
|
||||||
w.WriteHeader(appErr.StatusCode)
|
w.WriteHeader(appErr.StatusCode)
|
||||||
|
|
||||||
message := appErr.Message
|
message := appErr.Message
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HealthResponse represents the health check response
|
// HealthResponse represents the health check response
|
||||||
@@ -36,7 +36,7 @@ func (h *HealthHandler) Check(w http.ResponseWriter, r *http.Request) {
|
|||||||
Version: h.version,
|
Version: h.version,
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
|
w.Header().Set(c.HeaderContentType, c.ContentTypeJSON)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||||
log.Printf("ERROR encoding health check response: %v", err)
|
log.Printf("ERROR encoding health check response: %v", err)
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ package httputil
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Lang extracts and validates the language from query params.
|
// Lang extracts and validates the language from query params.
|
||||||
// Returns the language or default if invalid/missing.
|
// Returns the language or default if invalid/missing.
|
||||||
func Lang(r *http.Request) string {
|
func Lang(r *http.Request) string {
|
||||||
lang := r.URL.Query().Get("lang")
|
lang := r.URL.Query().Get("lang")
|
||||||
if lang == "" || !constants.SupportedLanguages[lang] {
|
if lang == "" || !c.SupportedLanguages[lang] {
|
||||||
return constants.LangDefault
|
return c.LangDefault
|
||||||
}
|
}
|
||||||
return lang
|
return lang
|
||||||
}
|
}
|
||||||
@@ -22,9 +22,9 @@ func Lang(r *http.Request) string {
|
|||||||
func LangOrError(r *http.Request) (string, bool) {
|
func LangOrError(r *http.Request) (string, bool) {
|
||||||
lang := r.URL.Query().Get("lang")
|
lang := r.URL.Query().Get("lang")
|
||||||
if lang == "" {
|
if lang == "" {
|
||||||
return constants.LangDefault, true
|
return c.LangDefault, true
|
||||||
}
|
}
|
||||||
if !constants.SupportedLanguages[lang] {
|
if !c.SupportedLanguages[lang] {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
return lang, true
|
return lang, true
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ package httputil
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
// JSON writes a JSON response with the given status code.
|
// JSON writes a JSON response with the given status code.
|
||||||
func JSON(w http.ResponseWriter, status int, data interface{}) error {
|
func JSON(w http.ResponseWriter, status int, data interface{}) error {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set(c.HeaderContentType, c.ContentTypeJSON)
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
return json.NewEncoder(w).Encode(data)
|
return json.NewEncoder(w).Encode(data)
|
||||||
}
|
}
|
||||||
@@ -19,13 +22,13 @@ func JSONOk(w http.ResponseWriter, data interface{}) error {
|
|||||||
|
|
||||||
// JSONCached writes a JSON response with caching headers.
|
// JSONCached writes a JSON response with caching headers.
|
||||||
func JSONCached(w http.ResponseWriter, data interface{}, maxAge int) error {
|
func JSONCached(w http.ResponseWriter, data interface{}, maxAge int) error {
|
||||||
w.Header().Set("Cache-Control", "public, max-age="+string(rune(maxAge)))
|
w.Header().Set(c.HeaderCacheControl, fmt.Sprintf("public, max-age=%d", maxAge))
|
||||||
return JSONOk(w, data)
|
return JSONOk(w, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML sets HTML content type header.
|
// HTML sets HTML content type header.
|
||||||
func HTML(w http.ResponseWriter) {
|
func HTML(w http.ResponseWriter) {
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set(c.HeaderContentType, c.ContentTypeHTML)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoContent sends a 204 No Content response.
|
// NoContent sends a 204 No Content response.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -23,7 +23,7 @@ const (
|
|||||||
func BrowserOnly(next http.Handler) http.Handler {
|
func BrowserOnly(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Check 1: User-Agent validation
|
// Check 1: User-Agent validation
|
||||||
userAgent := r.Header.Get(constants.HeaderUserAgent)
|
userAgent := r.Header.Get(c.HeaderUserAgent)
|
||||||
if userAgent == "" || isBotUserAgent(userAgent) {
|
if userAgent == "" || isBotUserAgent(userAgent) {
|
||||||
log.Printf("SECURITY: Blocked non-browser User-Agent from IP %s: %s", getRequestIP(r), userAgent)
|
log.Printf("SECURITY: Blocked non-browser User-Agent from IP %s: %s", getRequestIP(r), userAgent)
|
||||||
http.Error(w, "Forbidden: Browser access only", http.StatusForbidden)
|
http.Error(w, "Forbidden: Browser access only", http.StatusForbidden)
|
||||||
@@ -31,8 +31,8 @@ func BrowserOnly(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check 2: Require Referer or Origin header
|
// Check 2: Require Referer or Origin header
|
||||||
referer := r.Header.Get(constants.HeaderReferer)
|
referer := r.Header.Get(c.HeaderReferer)
|
||||||
origin := r.Header.Get(constants.HeaderOrigin)
|
origin := r.Header.Get(c.HeaderOrigin)
|
||||||
|
|
||||||
if referer == "" && origin == "" {
|
if referer == "" && origin == "" {
|
||||||
log.Printf("SECURITY: Blocked request without Referer/Origin from IP %s", getRequestIP(r))
|
log.Printf("SECURITY: Blocked request without Referer/Origin from IP %s", getRequestIP(r))
|
||||||
@@ -43,7 +43,7 @@ func BrowserOnly(next http.Handler) http.Handler {
|
|||||||
// Check 3: Custom header validation (set by JavaScript)
|
// Check 3: Custom header validation (set by JavaScript)
|
||||||
// For HTMX requests, check HX-Request header
|
// For HTMX requests, check HX-Request header
|
||||||
// For fetch/XMLHttpRequest, check X-Requested-With header
|
// For fetch/XMLHttpRequest, check X-Requested-With header
|
||||||
hasHTMXHeader := r.Header.Get(constants.HeaderHXRequest) == "true"
|
hasHTMXHeader := r.Header.Get(c.HeaderHXRequest) == "true"
|
||||||
hasXMLHTTPHeader := r.Header.Get(browserHeaderName) == browserHeaderValue
|
hasXMLHTTPHeader := r.Header.Get(browserHeaderName) == browserHeaderValue
|
||||||
hasCustomBrowserHeader := r.Header.Get("X-Browser-Request") == "true"
|
hasCustomBrowserHeader := r.Header.Get("X-Browser-Request") == "true"
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ func isBotUserAgent(ua string) bool {
|
|||||||
// getRequestIP extracts the client IP from the request
|
// getRequestIP extracts the client IP from the request
|
||||||
func getRequestIP(r *http.Request) string {
|
func getRequestIP(r *http.Request) string {
|
||||||
// Try X-Forwarded-For first (for proxies/load balancers)
|
// Try X-Forwarded-For first (for proxies/load balancers)
|
||||||
ip := r.Header.Get(constants.HeaderXForwardedFor)
|
ip := r.Header.Get(c.HeaderXForwardedFor)
|
||||||
if ip != "" {
|
if ip != "" {
|
||||||
// Take first IP if multiple
|
// Take first IP if multiple
|
||||||
ips := strings.Split(ip, ",")
|
ips := strings.Split(ip, ",")
|
||||||
@@ -106,7 +106,7 @@ func getRequestIP(r *http.Request) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try X-Real-IP
|
// Try X-Real-IP
|
||||||
ip = r.Header.Get(constants.HeaderXRealIP)
|
ip = r.Header.Get(c.HeaderXRealIP)
|
||||||
if ip != "" {
|
if ip != "" {
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
// contactRateLimitEntry tracks rate limiting for contact form per IP
|
// contactRateLimitEntry tracks rate limiting for contact form per IP
|
||||||
@@ -39,9 +39,9 @@ func NewContactRateLimiter() *ContactRateLimiter {
|
|||||||
func (rl *ContactRateLimiter) Middleware(next http.Handler) http.Handler {
|
func (rl *ContactRateLimiter) Middleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Get client IP (handle X-Forwarded-For for proxies)
|
// Get client IP (handle X-Forwarded-For for proxies)
|
||||||
ip := r.Header.Get(constants.HeaderXForwardedFor)
|
ip := r.Header.Get(c.HeaderXForwardedFor)
|
||||||
if ip == "" {
|
if ip == "" {
|
||||||
ip = r.Header.Get(constants.HeaderXRealIP)
|
ip = r.Header.Get(c.HeaderXRealIP)
|
||||||
}
|
}
|
||||||
if ip == "" {
|
if ip == "" {
|
||||||
ip = strings.Split(r.RemoteAddr, ":")[0]
|
ip = strings.Split(r.RemoteAddr, ":")[0]
|
||||||
@@ -54,18 +54,18 @@ func (rl *ContactRateLimiter) Middleware(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
if !rl.allow(ip) {
|
if !rl.allow(ip) {
|
||||||
// Check if HTMX request
|
// Check if HTMX request
|
||||||
isHTMX := r.Header.Get(constants.HeaderHXRequest) != ""
|
isHTMX := r.Header.Get(c.HeaderHXRequest) != ""
|
||||||
|
|
||||||
if isHTMX {
|
if isHTMX {
|
||||||
// Return HTMX-friendly error
|
// Return HTMX-friendly error
|
||||||
w.Header().Set(constants.HeaderContentType, constants.ContentTypeHTML)
|
w.Header().Set(c.HeaderContentType, c.ContentTypeHTML)
|
||||||
w.WriteHeader(http.StatusTooManyRequests)
|
w.WriteHeader(http.StatusTooManyRequests)
|
||||||
_, _ = w.Write([]byte(`<div class="alert alert-error">
|
_, _ = w.Write([]byte(`<div class="alert alert-error">
|
||||||
<h3>Too Many Requests</h3>
|
<h3>Too Many Requests</h3>
|
||||||
<p>You've submitted too many contact forms. Please wait an hour before trying again.</p>
|
<p>You've submitted too many contact forms. Please wait an hour before trying again.</p>
|
||||||
</div>`))
|
</div>`))
|
||||||
} else {
|
} else {
|
||||||
w.Header().Set(constants.HeaderRetryAfter, "3600") // 1 hour
|
w.Header().Set(c.HeaderRetryAfter, "3600") // 1 hour
|
||||||
http.Error(w, "Too many contact form submissions. Please try again in an hour.", http.StatusTooManyRequests)
|
http.Error(w, "Too many contact form submissions. Please try again in an hour.", http.StatusTooManyRequests)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
+24
-22
@@ -8,6 +8,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -44,18 +46,18 @@ func NewCSRFProtection() *CSRFProtection {
|
|||||||
// Middleware provides CSRF protection for state-changing operations
|
// Middleware provides CSRF protection for state-changing operations
|
||||||
// GET requests: Generate and set CSRF token
|
// GET requests: Generate and set CSRF token
|
||||||
// POST/PUT/DELETE: Validate CSRF token
|
// POST/PUT/DELETE: Validate CSRF token
|
||||||
func (c *CSRFProtection) Middleware(next http.Handler) http.Handler {
|
func (csrf *CSRFProtection) Middleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Only validate on state-changing methods
|
// Only validate on state-changing methods
|
||||||
if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodDelete {
|
if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodDelete {
|
||||||
if !c.validateToken(r) {
|
if !csrf.validateToken(r) {
|
||||||
log.Printf("SECURITY: CSRF validation failed from IP %s", getClientIP(r))
|
log.Printf("SECURITY: CSRF validation failed from IP %s", getClientIP(r))
|
||||||
|
|
||||||
// Check if HTMX request
|
// Check if HTMX request
|
||||||
isHTMX := r.Header.Get("HX-Request") != ""
|
isHTMX := r.Header.Get(c.HeaderHXRequest) != ""
|
||||||
|
|
||||||
if isHTMX {
|
if isHTMX {
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set(c.HeaderContentType, c.ContentTypeHTML)
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
_, _ = w.Write([]byte(`<div class="alert alert-error">
|
_, _ = w.Write([]byte(`<div class="alert alert-error">
|
||||||
<h3>Security Error</h3>
|
<h3>Security Error</h3>
|
||||||
@@ -73,7 +75,7 @@ func (c *CSRFProtection) Middleware(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generateToken creates a new CSRF token
|
// generateToken creates a new CSRF token
|
||||||
func (c *CSRFProtection) generateToken() (string, error) {
|
func (csrf *CSRFProtection) generateToken() (string, error) {
|
||||||
bytes := make([]byte, csrfTokenLength)
|
bytes := make([]byte, csrfTokenLength)
|
||||||
if _, err := rand.Read(bytes); err != nil {
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -82,26 +84,26 @@ func (c *CSRFProtection) generateToken() (string, error) {
|
|||||||
token := base64.URLEncoding.EncodeToString(bytes)
|
token := base64.URLEncoding.EncodeToString(bytes)
|
||||||
|
|
||||||
// Store token with expiration
|
// Store token with expiration
|
||||||
c.mu.Lock()
|
csrf.mu.Lock()
|
||||||
c.tokens[token] = &csrfTokenEntry{
|
csrf.tokens[token] = &csrfTokenEntry{
|
||||||
token: token,
|
token: token,
|
||||||
expiresAt: time.Now().Add(csrfTokenTTL),
|
expiresAt: time.Now().Add(csrfTokenTTL),
|
||||||
}
|
}
|
||||||
c.mu.Unlock()
|
csrf.mu.Unlock()
|
||||||
|
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetToken retrieves or generates a CSRF token for the request
|
// GetToken retrieves or generates a CSRF token for the request
|
||||||
// This should be called when rendering forms
|
// This should be called when rendering forms
|
||||||
func (c *CSRFProtection) GetToken(w http.ResponseWriter, r *http.Request) (string, error) {
|
func (csrf *CSRFProtection) GetToken(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||||
// Check if token exists in cookie
|
// Check if token exists in cookie
|
||||||
cookie, err := r.Cookie(csrfCookieName)
|
cookie, err := r.Cookie(csrfCookieName)
|
||||||
if err == nil && cookie.Value != "" {
|
if err == nil && cookie.Value != "" {
|
||||||
// Validate existing token
|
// Validate existing token
|
||||||
c.mu.RLock()
|
csrf.mu.RLock()
|
||||||
entry, exists := c.tokens[cookie.Value]
|
entry, exists := csrf.tokens[cookie.Value]
|
||||||
c.mu.RUnlock()
|
csrf.mu.RUnlock()
|
||||||
|
|
||||||
if exists && time.Now().Before(entry.expiresAt) {
|
if exists && time.Now().Before(entry.expiresAt) {
|
||||||
// Token is valid, return it
|
// Token is valid, return it
|
||||||
@@ -110,7 +112,7 @@ func (c *CSRFProtection) GetToken(w http.ResponseWriter, r *http.Request) (strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate new token
|
// Generate new token
|
||||||
token, err := c.generateToken()
|
token, err := csrf.generateToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to generate CSRF token: %w", err)
|
return "", fmt.Errorf("failed to generate CSRF token: %w", err)
|
||||||
}
|
}
|
||||||
@@ -130,7 +132,7 @@ func (c *CSRFProtection) GetToken(w http.ResponseWriter, r *http.Request) (strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validateToken validates the CSRF token from the request
|
// validateToken validates the CSRF token from the request
|
||||||
func (c *CSRFProtection) validateToken(r *http.Request) bool {
|
func (csrf *CSRFProtection) validateToken(r *http.Request) bool {
|
||||||
// Get token from form
|
// Get token from form
|
||||||
var formToken string
|
var formToken string
|
||||||
|
|
||||||
@@ -163,9 +165,9 @@ func (c *CSRFProtection) validateToken(r *http.Request) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate token exists and is not expired
|
// Validate token exists and is not expired
|
||||||
c.mu.RLock()
|
csrf.mu.RLock()
|
||||||
entry, exists := c.tokens[formToken]
|
entry, exists := csrf.tokens[formToken]
|
||||||
c.mu.RUnlock()
|
csrf.mu.RUnlock()
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
log.Printf("CSRF: Token not found in store")
|
log.Printf("CSRF: Token not found in store")
|
||||||
@@ -181,19 +183,19 @@ func (c *CSRFProtection) validateToken(r *http.Request) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// cleanup removes expired tokens periodically
|
// cleanup removes expired tokens periodically
|
||||||
func (c *CSRFProtection) cleanup() {
|
func (csrf *CSRFProtection) cleanup() {
|
||||||
ticker := time.NewTicker(1 * time.Hour)
|
ticker := time.NewTicker(1 * time.Hour)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
c.mu.Lock()
|
csrf.mu.Lock()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for token, entry := range c.tokens {
|
for token, entry := range csrf.tokens {
|
||||||
if now.After(entry.expiresAt) {
|
if now.After(entry.expiresAt) {
|
||||||
delete(c.tokens, token)
|
delete(csrf.tokens, token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.mu.Unlock()
|
csrf.mu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
// contextKey is a private type for context keys to avoid collisions
|
// contextKey is a private type for context keys to avoid collisions
|
||||||
@@ -30,22 +30,22 @@ type Preferences struct {
|
|||||||
func PreferencesMiddleware(next http.Handler) http.Handler {
|
func PreferencesMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
prefs := &Preferences{
|
prefs := &Preferences{
|
||||||
CVLength: getPreferenceCookie(r, constants.CookieCVLength, constants.CVLengthShort),
|
CVLength: getPreferenceCookie(r, c.CookieCVLength, c.CVLengthShort),
|
||||||
CVIcons: getPreferenceCookie(r, constants.CookieCVIcons, constants.CVIconsShow),
|
CVIcons: getPreferenceCookie(r, c.CookieCVIcons, c.CVIconsShow),
|
||||||
CVLanguage: getPreferenceCookie(r, constants.CookieCVLanguage, constants.LangEnglish),
|
CVLanguage: getPreferenceCookie(r, c.CookieCVLanguage, c.LangEnglish),
|
||||||
CVTheme: getPreferenceCookie(r, constants.CookieCVTheme, constants.CVThemeDefault),
|
CVTheme: getPreferenceCookie(r, c.CookieCVTheme, c.CVThemeDefault),
|
||||||
ColorTheme: getPreferenceCookie(r, constants.CookieColorTheme, constants.ColorThemeLight),
|
ColorTheme: getPreferenceCookie(r, c.CookieColorTheme, c.ColorThemeLight),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate old preference values (one-time auto-migration)
|
// Migrate old preference values (one-time auto-migration)
|
||||||
if prefs.CVLength == "extended" {
|
if prefs.CVLength == "extended" {
|
||||||
prefs.CVLength = constants.CVLengthLong
|
prefs.CVLength = c.CVLengthLong
|
||||||
}
|
}
|
||||||
switch prefs.CVIcons {
|
switch prefs.CVIcons {
|
||||||
case "true":
|
case "true":
|
||||||
prefs.CVIcons = constants.CVIconsShow
|
prefs.CVIcons = c.CVIconsShow
|
||||||
case "false":
|
case "false":
|
||||||
prefs.CVIcons = constants.CVIconsHide
|
prefs.CVIcons = c.CVIconsHide
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store preferences in context
|
// Store preferences in context
|
||||||
@@ -60,11 +60,11 @@ func GetPreferences(r *http.Request) *Preferences {
|
|||||||
if !ok {
|
if !ok {
|
||||||
// Return default preferences if not found
|
// Return default preferences if not found
|
||||||
return &Preferences{
|
return &Preferences{
|
||||||
CVLength: constants.CVLengthShort,
|
CVLength: c.CVLengthShort,
|
||||||
CVIcons: constants.CVIconsShow,
|
CVIcons: c.CVIconsShow,
|
||||||
CVLanguage: constants.LangEnglish,
|
CVLanguage: c.LangEnglish,
|
||||||
CVTheme: constants.CVThemeDefault,
|
CVTheme: c.CVThemeDefault,
|
||||||
ColorTheme: constants.ColorThemeLight,
|
ColorTheme: c.ColorThemeLight,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return prefs
|
return prefs
|
||||||
@@ -102,42 +102,42 @@ func GetColorTheme(r *http.Request) string {
|
|||||||
|
|
||||||
// IsLongCV returns true if the user prefers long CV format
|
// IsLongCV returns true if the user prefers long CV format
|
||||||
func IsLongCV(r *http.Request) bool {
|
func IsLongCV(r *http.Request) bool {
|
||||||
return GetCVLength(r) == constants.CVLengthLong
|
return GetCVLength(r) == c.CVLengthLong
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsShortCV returns true if the user prefers short CV format
|
// IsShortCV returns true if the user prefers short CV format
|
||||||
func IsShortCV(r *http.Request) bool {
|
func IsShortCV(r *http.Request) bool {
|
||||||
return GetCVLength(r) == constants.CVLengthShort
|
return GetCVLength(r) == c.CVLengthShort
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShowIcons returns true if icons should be visible
|
// ShowIcons returns true if icons should be visible
|
||||||
func ShowIcons(r *http.Request) bool {
|
func ShowIcons(r *http.Request) bool {
|
||||||
return GetCVIcons(r) == constants.CVIconsShow
|
return GetCVIcons(r) == c.CVIconsShow
|
||||||
}
|
}
|
||||||
|
|
||||||
// HideIcons returns true if icons should be hidden
|
// HideIcons returns true if icons should be hidden
|
||||||
func HideIcons(r *http.Request) bool {
|
func HideIcons(r *http.Request) bool {
|
||||||
return GetCVIcons(r) == constants.CVIconsHide
|
return GetCVIcons(r) == c.CVIconsHide
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsCleanTheme returns true if clean theme is selected
|
// IsCleanTheme returns true if clean theme is selected
|
||||||
func IsCleanTheme(r *http.Request) bool {
|
func IsCleanTheme(r *http.Request) bool {
|
||||||
return GetCVTheme(r) == constants.CVThemeClean
|
return GetCVTheme(r) == c.CVThemeClean
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsDefaultTheme returns true if default theme is selected
|
// IsDefaultTheme returns true if default theme is selected
|
||||||
func IsDefaultTheme(r *http.Request) bool {
|
func IsDefaultTheme(r *http.Request) bool {
|
||||||
return GetCVTheme(r) == constants.CVThemeDefault
|
return GetCVTheme(r) == c.CVThemeDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsDarkMode returns true if dark mode is enabled
|
// IsDarkMode returns true if dark mode is enabled
|
||||||
func IsDarkMode(r *http.Request) bool {
|
func IsDarkMode(r *http.Request) bool {
|
||||||
return GetColorTheme(r) == constants.ColorThemeDark
|
return GetColorTheme(r) == c.ColorThemeDark
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLightMode returns true if light mode is enabled
|
// IsLightMode returns true if light mode is enabled
|
||||||
func IsLightMode(r *http.Request) bool {
|
func IsLightMode(r *http.Request) bool {
|
||||||
return GetColorTheme(r) == constants.ColorThemeLight
|
return GetColorTheme(r) == c.ColorThemeLight
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPreferenceCookie sets a preference cookie (1 year expiry)
|
// SetPreferenceCookie sets a preference cookie (1 year expiry)
|
||||||
@@ -145,8 +145,8 @@ func SetPreferenceCookie(w http.ResponseWriter, name string, value string) {
|
|||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: name,
|
Name: name,
|
||||||
Value: value,
|
Value: value,
|
||||||
Path: constants.CookiePath,
|
Path: c.CookiePath,
|
||||||
MaxAge: constants.CookieMaxAge,
|
MaxAge: c.CookieMaxAge,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
SameSite: http.SameSiteStrictMode,
|
SameSite: http.SameSiteStrictMode,
|
||||||
Secure: isProductionMode(), // Secure in production with HTTPS
|
Secure: isProductionMode(), // Secure in production with HTTPS
|
||||||
@@ -155,8 +155,8 @@ func SetPreferenceCookie(w http.ResponseWriter, name string, value string) {
|
|||||||
|
|
||||||
// isProductionMode checks if the application is running in production
|
// isProductionMode checks if the application is running in production
|
||||||
func isProductionMode() bool {
|
func isProductionMode() bool {
|
||||||
env := os.Getenv(constants.EnvVarGOEnv)
|
env := os.Getenv(c.EnvVarGOEnv)
|
||||||
return env == constants.EnvProduction || env == "prod"
|
return env == c.EnvProduction || env == "prod"
|
||||||
}
|
}
|
||||||
|
|
||||||
// getPreferenceCookie gets a preference cookie value, returns default if not found
|
// getPreferenceCookie gets a preference cookie value, returns default if not found
|
||||||
|
|||||||
@@ -7,26 +7,26 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SecurityHeaders adds production-grade security headers to responses
|
// SecurityHeaders adds production-grade security headers to responses
|
||||||
func SecurityHeaders(next http.Handler) http.Handler {
|
func SecurityHeaders(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Prevent clickjacking
|
// Prevent clickjacking
|
||||||
w.Header().Set(constants.HeaderXFrameOptions, constants.FrameOptionsSameOrigin)
|
w.Header().Set(c.HeaderXFrameOptions, c.FrameOptionsSameOrigin)
|
||||||
|
|
||||||
// Prevent MIME type sniffing
|
// Prevent MIME type sniffing
|
||||||
w.Header().Set(constants.HeaderXContentTypeOpts, constants.NoSniff)
|
w.Header().Set(c.HeaderXContentTypeOpts, c.NoSniff)
|
||||||
|
|
||||||
// XSS Protection (legacy but still useful for older browsers)
|
// XSS Protection (legacy but still useful for older browsers)
|
||||||
w.Header().Set(constants.HeaderXXSSProtection, constants.XSSProtection)
|
w.Header().Set(c.HeaderXXSSProtection, c.XSSProtection)
|
||||||
|
|
||||||
// Referrer policy - strict privacy
|
// Referrer policy - strict privacy
|
||||||
w.Header().Set(constants.HeaderReferrerPolicy, constants.ReferrerPolicy)
|
w.Header().Set(c.HeaderReferrerPolicy, c.ReferrerPolicy)
|
||||||
|
|
||||||
// Permissions Policy - disable unnecessary features
|
// Permissions Policy - disable unnecessary features
|
||||||
w.Header().Set(constants.HeaderPermissionsPolicy,
|
w.Header().Set(c.HeaderPermissionsPolicy,
|
||||||
"geolocation=(), microphone=(), camera=(), payment=(), usb=(), "+
|
"geolocation=(), microphone=(), camera=(), payment=(), usb=(), "+
|
||||||
"magnetometer=(), gyroscope=(), accelerometer=()")
|
"magnetometer=(), gyroscope=(), accelerometer=()")
|
||||||
|
|
||||||
@@ -40,12 +40,12 @@ func SecurityHeaders(next http.Handler) http.Handler {
|
|||||||
"frame-ancestors 'self'; " +
|
"frame-ancestors 'self'; " +
|
||||||
"base-uri 'self'; " +
|
"base-uri 'self'; " +
|
||||||
"form-action 'self'"
|
"form-action 'self'"
|
||||||
w.Header().Set(constants.HeaderCSP, csp)
|
w.Header().Set(c.HeaderCSP, csp)
|
||||||
|
|
||||||
// HSTS - only in production with HTTPS
|
// HSTS - only in production with HTTPS
|
||||||
if os.Getenv(constants.EnvVarGOEnv) == constants.EnvProduction {
|
if os.Getenv(c.EnvVarGOEnv) == c.EnvProduction {
|
||||||
// 1 year max-age, include subdomains
|
// 1 year max-age, include subdomains
|
||||||
w.Header().Set(constants.HeaderHSTS, constants.HSTSMaxAge)
|
w.Header().Set(c.HeaderHSTS, c.HSTSMaxAge)
|
||||||
}
|
}
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
@@ -76,7 +76,7 @@ func OriginChecker(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check Origin header (for CORS requests)
|
// Check Origin header (for CORS requests)
|
||||||
origin := r.Header.Get(constants.HeaderOrigin)
|
origin := r.Header.Get(c.HeaderOrigin)
|
||||||
if origin != "" {
|
if origin != "" {
|
||||||
if !isAllowedOrigin(origin, allowedOrigins) {
|
if !isAllowedOrigin(origin, allowedOrigins) {
|
||||||
http.Error(w, "Forbidden: External access not allowed", http.StatusForbidden)
|
http.Error(w, "Forbidden: External access not allowed", http.StatusForbidden)
|
||||||
@@ -85,7 +85,7 @@ func OriginChecker(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check Referer header (for direct requests)
|
// Check Referer header (for direct requests)
|
||||||
referer := r.Header.Get(constants.HeaderReferer)
|
referer := r.Header.Get(c.HeaderReferer)
|
||||||
if referer != "" {
|
if referer != "" {
|
||||||
if !isAllowedOrigin(referer, allowedOrigins) {
|
if !isAllowedOrigin(referer, allowedOrigins) {
|
||||||
http.Error(w, "Forbidden: External access not allowed", http.StatusForbidden)
|
http.Error(w, "Forbidden: External access not allowed", http.StatusForbidden)
|
||||||
@@ -98,7 +98,7 @@ func OriginChecker(next http.Handler) http.Handler {
|
|||||||
if origin == "" && referer == "" {
|
if origin == "" && referer == "" {
|
||||||
// For production, you might want to be stricter here
|
// For production, you might want to be stricter here
|
||||||
// For now, allow it (users can bookmark /export/pdf directly)
|
// For now, allow it (users can bookmark /export/pdf directly)
|
||||||
if os.Getenv(constants.EnvVarGOEnv) == constants.EnvProduction && r.URL.Path == constants.RouteExportPDF {
|
if os.Getenv(c.EnvVarGOEnv) == c.EnvProduction && r.URL.Path == c.RouteExportPDF {
|
||||||
// In production, require at least a referer for PDF endpoint
|
// In production, require at least a referer for PDF endpoint
|
||||||
http.Error(w, "Forbidden: Direct access not allowed", http.StatusForbidden)
|
http.Error(w, "Forbidden: Direct access not allowed", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
@@ -163,16 +163,16 @@ func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
|
|||||||
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
|
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Get client IP (handle X-Forwarded-For for proxies)
|
// Get client IP (handle X-Forwarded-For for proxies)
|
||||||
ip := r.Header.Get(constants.HeaderXForwardedFor)
|
ip := r.Header.Get(c.HeaderXForwardedFor)
|
||||||
if ip == "" {
|
if ip == "" {
|
||||||
ip = r.Header.Get(constants.HeaderXRealIP)
|
ip = r.Header.Get(c.HeaderXRealIP)
|
||||||
}
|
}
|
||||||
if ip == "" {
|
if ip == "" {
|
||||||
ip = strings.Split(r.RemoteAddr, ":")[0]
|
ip = strings.Split(r.RemoteAddr, ":")[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if !rl.allow(ip) {
|
if !rl.allow(ip) {
|
||||||
w.Header().Set(constants.HeaderRetryAfter, "60")
|
w.Header().Set(c.HeaderRetryAfter, "60")
|
||||||
http.Error(w, "Rate limit exceeded. Please try again later.", http.StatusTooManyRequests)
|
http.Error(w, "Rate limit exceeded. Please try again later.", http.StatusTooManyRequests)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -227,12 +227,12 @@ func (rl *RateLimiter) cleanup() {
|
|||||||
// 1 hour in development, 1 day in production
|
// 1 hour in development, 1 day in production
|
||||||
func CacheControl(next http.Handler) http.Handler {
|
func CacheControl(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
cacheValue := constants.CachePublic1Hour
|
cacheValue := c.CachePublic1Hour
|
||||||
if os.Getenv(constants.EnvVarGOEnv) == constants.EnvProduction {
|
if os.Getenv(c.EnvVarGOEnv) == c.EnvProduction {
|
||||||
cacheValue = constants.CachePublic1Day
|
cacheValue = c.CachePublic1Day
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set(constants.HeaderCacheControl, cacheValue)
|
w.Header().Set(c.HeaderCacheControl, cacheValue)
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -243,12 +243,12 @@ func DynamicCacheControl(next http.Handler) http.Handler {
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// For dynamic HTML pages: short cache, must revalidate
|
// For dynamic HTML pages: short cache, must revalidate
|
||||||
// This improves performance while ensuring fresh content
|
// This improves performance while ensuring fresh content
|
||||||
if os.Getenv(constants.EnvVarGOEnv) == constants.EnvProduction {
|
if os.Getenv(c.EnvVarGOEnv) == c.EnvProduction {
|
||||||
// Production: 5 minutes cache, but must revalidate
|
// Production: 5 minutes cache, but must revalidate
|
||||||
w.Header().Set(constants.HeaderCacheControl, constants.CachePublic5Min)
|
w.Header().Set(c.HeaderCacheControl, c.CachePublic5Min)
|
||||||
} else {
|
} else {
|
||||||
// Development: no cache for easier testing
|
// Development: no cache for easier testing
|
||||||
w.Header().Set(constants.HeaderCacheControl, constants.CacheNoStore)
|
w.Header().Set(c.HeaderCacheControl, c.CacheNoStore)
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanatsap/cv-site/internal/constants"
|
c "github.com/juanatsap/cv-site/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SecurityEvent represents a security-related event
|
// SecurityEvent represents a security-related event
|
||||||
@@ -57,7 +57,7 @@ func LogSecurityEvent(eventType string, r *http.Request, details string) {
|
|||||||
EventType: eventType,
|
EventType: eventType,
|
||||||
Severity: severity,
|
Severity: severity,
|
||||||
IP: getClientIP(r),
|
IP: getClientIP(r),
|
||||||
UserAgent: r.Header.Get(constants.HeaderUserAgent),
|
UserAgent: r.Header.Get(c.HeaderUserAgent),
|
||||||
Method: r.Method,
|
Method: r.Method,
|
||||||
Path: r.URL.Path,
|
Path: r.URL.Path,
|
||||||
Details: details,
|
Details: details,
|
||||||
@@ -74,7 +74,7 @@ func LogSecurityEvent(eventType string, r *http.Request, details string) {
|
|||||||
log.Printf("[SECURITY] %s", eventJSON)
|
log.Printf("[SECURITY] %s", eventJSON)
|
||||||
|
|
||||||
// Also log to separate security log file in production
|
// Also log to separate security log file in production
|
||||||
if os.Getenv(constants.EnvVarGOEnv) == constants.EnvProduction {
|
if os.Getenv(c.EnvVarGOEnv) == c.EnvProduction {
|
||||||
logToSecurityFile(eventJSON)
|
logToSecurityFile(eventJSON)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,14 +99,14 @@ func getSeverity(eventType string) string {
|
|||||||
// getClientIP extracts the real client IP from request headers
|
// getClientIP extracts the real client IP from request headers
|
||||||
func getClientIP(r *http.Request) string {
|
func getClientIP(r *http.Request) string {
|
||||||
// Check X-Forwarded-For header (proxy/load balancer)
|
// Check X-Forwarded-For header (proxy/load balancer)
|
||||||
if xff := r.Header.Get(constants.HeaderXForwardedFor); xff != "" {
|
if xff := r.Header.Get(c.HeaderXForwardedFor); xff != "" {
|
||||||
// Take first IP from comma-separated list
|
// Take first IP from comma-separated list
|
||||||
ips := strings.Split(xff, ",")
|
ips := strings.Split(xff, ",")
|
||||||
return strings.TrimSpace(ips[0])
|
return strings.TrimSpace(ips[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check X-Real-IP header
|
// Check X-Real-IP header
|
||||||
if xri := r.Header.Get(constants.HeaderXRealIP); xri != "" {
|
if xri := r.Header.Get(c.HeaderXRealIP); xri != "" {
|
||||||
return xri
|
return xri
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ func SecurityLogger(next http.Handler) http.Handler {
|
|||||||
EventType: "REQUEST",
|
EventType: "REQUEST",
|
||||||
Severity: SeverityInfo,
|
Severity: SeverityInfo,
|
||||||
IP: getClientIP(r),
|
IP: getClientIP(r),
|
||||||
UserAgent: r.Header.Get(constants.HeaderUserAgent),
|
UserAgent: r.Header.Get(c.HeaderUserAgent),
|
||||||
Method: r.Method,
|
Method: r.Method,
|
||||||
Path: r.URL.Path,
|
Path: r.URL.Path,
|
||||||
Details: string(detailsJSON),
|
Details: string(detailsJSON),
|
||||||
@@ -203,7 +203,7 @@ func SecurityLogger(next http.Handler) http.Handler {
|
|||||||
EventType: "HTTP_ERROR",
|
EventType: "HTTP_ERROR",
|
||||||
Severity: severity,
|
Severity: severity,
|
||||||
IP: getClientIP(r),
|
IP: getClientIP(r),
|
||||||
UserAgent: r.Header.Get(constants.HeaderUserAgent),
|
UserAgent: r.Header.Get(c.HeaderUserAgent),
|
||||||
Method: r.Method,
|
Method: r.Method,
|
||||||
Path: r.URL.Path,
|
Path: r.URL.Path,
|
||||||
Details: http.StatusText(wrapped.status),
|
Details: http.StatusText(wrapped.status),
|
||||||
|
|||||||
Reference in New Issue
Block a user