eb92f64e93
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
153 lines
4.6 KiB
Go
153 lines
4.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
)
|
|
|
|
// ==============================================================================
|
|
// REQUEST/RESPONSE TYPES
|
|
// Structured types for common request patterns with validation
|
|
// ==============================================================================
|
|
|
|
// LanguageRequest represents a request with language parameter
|
|
type LanguageRequest struct {
|
|
Lang string `validate:"required,oneof=en es"`
|
|
}
|
|
|
|
// ParseLanguageRequest parses and validates language from query parameters
|
|
func ParseLanguageRequest(r *http.Request) (*LanguageRequest, error) {
|
|
lang := r.URL.Query().Get("lang")
|
|
if lang == "" {
|
|
lang = "en"
|
|
}
|
|
|
|
// Validate language
|
|
if lang != "en" && lang != "es" {
|
|
return nil, fmt.Errorf("unsupported language: %s (use 'en' or 'es')", lang)
|
|
}
|
|
|
|
return &LanguageRequest{Lang: lang}, nil
|
|
}
|
|
|
|
// PDFExportRequest represents all parameters for PDF export
|
|
type PDFExportRequest struct {
|
|
Lang string `validate:"required,oneof=en es"` // Language: "en" or "es"
|
|
Length string `validate:"required,oneof=short long"` // Length: "short" or "long"
|
|
Icons string `validate:"required,oneof=show hide"` // Icons: "show" or "hide"
|
|
Version string `validate:"required,oneof=with_skills clean"` // Version: "with_skills" or "clean"
|
|
}
|
|
|
|
// ParsePDFExportRequest parses and validates PDF export parameters
|
|
func ParsePDFExportRequest(r *http.Request) (*PDFExportRequest, error) {
|
|
req := &PDFExportRequest{
|
|
Lang: r.URL.Query().Get("lang"),
|
|
Length: r.URL.Query().Get("length"),
|
|
Icons: r.URL.Query().Get("icons"),
|
|
Version: r.URL.Query().Get("version"),
|
|
}
|
|
|
|
// Set defaults
|
|
if req.Lang == "" {
|
|
req.Lang = "en"
|
|
}
|
|
if req.Length == "" {
|
|
req.Length = "short"
|
|
}
|
|
if req.Icons == "" {
|
|
req.Icons = "show"
|
|
}
|
|
if req.Version == "" {
|
|
req.Version = "with_skills"
|
|
}
|
|
|
|
// Validate language
|
|
if req.Lang != "en" && req.Lang != "es" {
|
|
return nil, fmt.Errorf("unsupported language: %s (use 'en' or 'es')", req.Lang)
|
|
}
|
|
|
|
// Validate length
|
|
if req.Length != "short" && req.Length != "long" {
|
|
return nil, fmt.Errorf("unsupported length: %s (use 'short' or 'long')", req.Length)
|
|
}
|
|
|
|
// Validate icons
|
|
if req.Icons != "show" && req.Icons != "hide" {
|
|
return nil, fmt.Errorf("unsupported icons option: %s (use 'show' or 'hide')", req.Icons)
|
|
}
|
|
|
|
// Validate version
|
|
if req.Version != "with_skills" && req.Version != "clean" {
|
|
return nil, fmt.Errorf("unsupported version: %s (use 'with_skills' or 'clean')", req.Version)
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
// ==============================================================================
|
|
// RESPONSE TYPES
|
|
// Structured response types for consistent API responses
|
|
// ==============================================================================
|
|
|
|
// APIResponse is a standardized response wrapper for JSON responses
|
|
type APIResponse struct {
|
|
Success bool `json:"success"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
Error *ErrorInfo `json:"error,omitempty"`
|
|
Meta *MetaInfo `json:"meta,omitempty"`
|
|
}
|
|
|
|
// ErrorInfo provides structured error information
|
|
type ErrorInfo struct {
|
|
Code string `json:"code"` // Error code (e.g., "INVALID_LANGUAGE")
|
|
Message string `json:"message"` // Human-readable error message
|
|
Field string `json:"field,omitempty"` // Field that caused the error
|
|
Details string `json:"details,omitempty"` // Additional error details
|
|
}
|
|
|
|
// MetaInfo provides metadata about the response
|
|
type MetaInfo struct {
|
|
Timestamp int64 `json:"timestamp,omitempty"` // Unix timestamp
|
|
Version string `json:"version,omitempty"` // API version
|
|
RequestID string `json:"request_id,omitempty"` // Request tracking ID
|
|
}
|
|
|
|
// SuccessResponse creates a success response
|
|
func SuccessResponse(data interface{}) *APIResponse {
|
|
return &APIResponse{
|
|
Success: true,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
// NewErrorResponse creates an error response
|
|
func NewErrorResponse(code, message string) *APIResponse {
|
|
return &APIResponse{
|
|
Success: false,
|
|
Error: &ErrorInfo{
|
|
Code: code,
|
|
Message: message,
|
|
},
|
|
}
|
|
}
|
|
|
|
// ErrorResponseWithField creates an error response with field information
|
|
func ErrorResponseWithField(code, message, field string) *APIResponse {
|
|
return &APIResponse{
|
|
Success: false,
|
|
Error: &ErrorInfo{
|
|
Code: code,
|
|
Message: message,
|
|
Field: field,
|
|
},
|
|
}
|
|
}
|
|
|
|
// HealthCheckResponse represents health check endpoint response
|
|
type HealthCheckResponse struct {
|
|
Status string `json:"status"` // "healthy" or "unhealthy"
|
|
Version string `json:"version"` // Application version
|
|
Uptime int64 `json:"uptime,omitempty"` // Uptime in seconds
|
|
Checks map[string]bool `json:"checks,omitempty"` // Component health checks
|
|
}
|