Files
cv-site/internal/middleware/preferences.go
T
juanatsap eb92f64e93 fix: Mobile hamburger menu and iPad sidebar visibility
Mobile fixes:
- Add click toggle handler for hamburger menu (was hover-only)
- Menu now opens/closes on tap and closes when clicking outside
- Keep hover support for desktop

iPad fixes:
- Sidebar content now visible on touch devices (901-1280px)
- Added (hover: hover) media query to prevent hide-on-hover on tablets

Security improvements:
- Replace exec.CommandContext with go-git library for git operations
- Add path traversal and command injection prevention
- Fix race condition in template hot reload
- Add environment-based cookie Secure flag

Code quality:
- Add constants.go for magic numbers
- Remove unused code (ParsePreferenceToggleRequest, DomainError)
- Add FOUC prevention with inline critical CSS
- Add Makefile dev/run/clean targets
- Fix README git clone URL
- Add doc/DECISIONS.md for architectural decisions

Tests:
- Add hamburger menu click toggle tests
- Add iPad sidebar visibility tests
- Update security tests for go-git implementation
- Add cookie Secure flag tests
2025-11-30 09:29:35 +00:00

168 lines
4.7 KiB
Go

package middleware
import (
"context"
"net/http"
"os"
)
// contextKey is a private type for context keys to avoid collisions
type contextKey string
const (
// PreferencesKey is the context key for user preferences
PreferencesKey contextKey = "preferences"
)
// Preferences holds user preference values from cookies
type Preferences struct {
CVLength string // "short" or "long"
CVIcons string // "show" or "hide"
CVLanguage string // "en" or "es"
CVTheme string // "default" or "clean"
ColorTheme string // "light" or "dark"
}
// PreferencesMiddleware reads user preferences from cookies and stores them in context
// This eliminates the need for handlers to manually read cookies
func PreferencesMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
prefs := &Preferences{
CVLength: getPreferenceCookie(r, "cv-length", "short"),
CVIcons: getPreferenceCookie(r, "cv-icons", "show"),
CVLanguage: getPreferenceCookie(r, "cv-language", "en"),
CVTheme: getPreferenceCookie(r, "cv-theme", "default"),
ColorTheme: getPreferenceCookie(r, "color-theme", "light"),
}
// Migrate old preference values (one-time auto-migration)
if prefs.CVLength == "extended" {
prefs.CVLength = "long"
}
switch prefs.CVIcons {
case "true":
prefs.CVIcons = "show"
case "false":
prefs.CVIcons = "hide"
}
// Store preferences in context
ctx := context.WithValue(r.Context(), PreferencesKey, prefs)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// GetPreferences retrieves preferences from request context
func GetPreferences(r *http.Request) *Preferences {
prefs, ok := r.Context().Value(PreferencesKey).(*Preferences)
if !ok {
// Return default preferences if not found
return &Preferences{
CVLength: "short",
CVIcons: "show",
CVLanguage: "en",
CVTheme: "default",
ColorTheme: "light",
}
}
return prefs
}
// ==============================================================================
// CONTEXT HELPER FUNCTIONS
// Convenience functions for accessing specific preference values
// ==============================================================================
// GetLanguage retrieves the user's language preference
func GetLanguage(r *http.Request) string {
return GetPreferences(r).CVLanguage
}
// GetCVLength retrieves the user's CV length preference
func GetCVLength(r *http.Request) string {
return GetPreferences(r).CVLength
}
// GetCVIcons retrieves the user's icon visibility preference
func GetCVIcons(r *http.Request) string {
return GetPreferences(r).CVIcons
}
// GetCVTheme retrieves the user's CV theme preference
func GetCVTheme(r *http.Request) string {
return GetPreferences(r).CVTheme
}
// GetColorTheme retrieves the user's color theme preference
func GetColorTheme(r *http.Request) string {
return GetPreferences(r).ColorTheme
}
// IsLongCV returns true if the user prefers long CV format
func IsLongCV(r *http.Request) bool {
return GetCVLength(r) == "long"
}
// IsShortCV returns true if the user prefers short CV format
func IsShortCV(r *http.Request) bool {
return GetCVLength(r) == "short"
}
// ShowIcons returns true if icons should be visible
func ShowIcons(r *http.Request) bool {
return GetCVIcons(r) == "show"
}
// HideIcons returns true if icons should be hidden
func HideIcons(r *http.Request) bool {
return GetCVIcons(r) == "hide"
}
// IsCleanTheme returns true if clean theme is selected
func IsCleanTheme(r *http.Request) bool {
return GetCVTheme(r) == "clean"
}
// IsDefaultTheme returns true if default theme is selected
func IsDefaultTheme(r *http.Request) bool {
return GetCVTheme(r) == "default"
}
// IsDarkMode returns true if dark mode is enabled
func IsDarkMode(r *http.Request) bool {
return GetColorTheme(r) == "dark"
}
// IsLightMode returns true if light mode is enabled
func IsLightMode(r *http.Request) bool {
return GetColorTheme(r) == "light"
}
// SetPreferenceCookie sets a preference cookie (1 year expiry)
func SetPreferenceCookie(w http.ResponseWriter, name string, value string) {
http.SetCookie(w, &http.Cookie{
Name: name,
Value: value,
Path: "/",
MaxAge: 365 * 24 * 60 * 60, // 1 year
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Secure: isProductionMode(), // Secure in production with HTTPS
})
}
// isProductionMode checks if the application is running in production
func isProductionMode() bool {
env := os.Getenv("GO_ENV")
return env == "production" || env == "prod"
}
// getPreferenceCookie gets a preference cookie value, returns default if not found
func getPreferenceCookie(r *http.Request, name string, defaultValue string) string {
cookie, err := r.Cookie(name)
if err != nil {
return defaultValue
}
return cookie.Value
}