c89b67a06d
- Merge lang package into constants (add IsValidLang, ValidateLang, AllLangs) - Rename internal/services to internal/email for consistency with pdf package - Rename types to avoid redundancy: EmailService→Service, EmailConfig→Config - Update all imports and references across codebase - Delete internal/lang directory (functions moved to constants)
218 lines
5.6 KiB
Go
218 lines
5.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
|
|
c "github.com/juanatsap/cv-site/internal/constants"
|
|
)
|
|
|
|
// ErrorResponse represents a structured error response
|
|
type ErrorResponse struct {
|
|
Error string `json:"error"`
|
|
Message string `json:"message,omitempty"`
|
|
Code int `json:"code"`
|
|
}
|
|
|
|
// AppError represents an application error with context
|
|
type AppError struct {
|
|
Err error
|
|
Message string
|
|
StatusCode int
|
|
Internal bool // If true, don't expose details to client
|
|
}
|
|
|
|
// Error implements the error interface
|
|
func (e *AppError) Error() string {
|
|
if e.Err != nil {
|
|
return e.Err.Error()
|
|
}
|
|
return e.Message
|
|
}
|
|
|
|
// NewAppError creates a new application error
|
|
func NewAppError(err error, message string, statusCode int, internal bool) *AppError {
|
|
return &AppError{
|
|
Err: err,
|
|
Message: message,
|
|
StatusCode: statusCode,
|
|
Internal: internal,
|
|
}
|
|
}
|
|
|
|
// HandleError handles errors consistently across the application
|
|
func HandleError(w http.ResponseWriter, r *http.Request, err error) {
|
|
var appErr *AppError
|
|
|
|
// Check if it's an AppError
|
|
switch e := err.(type) {
|
|
case *AppError:
|
|
appErr = e
|
|
default:
|
|
// Unknown error - treat as internal server error
|
|
appErr = NewAppError(err, "Internal Server Error", http.StatusInternalServerError, true)
|
|
}
|
|
|
|
// Log the error
|
|
if appErr.Internal {
|
|
log.Printf("ERROR [%s %s]: %v", r.Method, r.URL.Path, appErr.Err)
|
|
} else {
|
|
log.Printf("CLIENT ERROR [%s %s]: %s (status: %d)", r.Method, r.URL.Path, appErr.Message, appErr.StatusCode)
|
|
}
|
|
|
|
// Determine response based on Accept header
|
|
accept := r.Header.Get(c.HeaderAccept)
|
|
isJSON := accept == c.ContentTypeJSON
|
|
isHTMX := r.Header.Get(c.HeaderHXRequest) != ""
|
|
|
|
if isJSON {
|
|
// JSON response
|
|
w.Header().Set(c.HeaderContentType, c.ContentTypeJSON)
|
|
w.WriteHeader(appErr.StatusCode)
|
|
|
|
response := ErrorResponse{
|
|
Error: http.StatusText(appErr.StatusCode),
|
|
Code: appErr.StatusCode,
|
|
}
|
|
|
|
// Only include message if not internal
|
|
if !appErr.Internal {
|
|
response.Message = appErr.Message
|
|
}
|
|
|
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
|
log.Printf("ERROR encoding JSON response: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if isHTMX {
|
|
// HTMX response - return simple HTML fragment
|
|
w.Header().Set(c.HeaderContentType, c.ContentTypeHTMLFragment)
|
|
w.WriteHeader(appErr.StatusCode)
|
|
|
|
message := appErr.Message
|
|
if appErr.Internal {
|
|
message = "An error occurred. Please try again later."
|
|
}
|
|
|
|
if _, err := w.Write([]byte("<div class='error'>" + message + "</div>")); err != nil {
|
|
log.Printf("ERROR writing HTMX error response: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Standard HTTP error response
|
|
message := appErr.Message
|
|
if appErr.Internal {
|
|
message = "Internal Server Error"
|
|
}
|
|
|
|
http.Error(w, message, appErr.StatusCode)
|
|
}
|
|
|
|
// Common error constructors
|
|
|
|
func NotFoundError(message string) *AppError {
|
|
return NewAppError(nil, message, http.StatusNotFound, false)
|
|
}
|
|
|
|
func BadRequestError(message string) *AppError {
|
|
return NewAppError(nil, message, http.StatusBadRequest, false)
|
|
}
|
|
|
|
func InternalError(err error) *AppError {
|
|
return NewAppError(err, "Internal Server Error", http.StatusInternalServerError, true)
|
|
}
|
|
|
|
func TemplateError(err error, templateName string) *AppError {
|
|
return NewAppError(
|
|
err,
|
|
"Error rendering template: "+templateName,
|
|
http.StatusInternalServerError,
|
|
true,
|
|
)
|
|
}
|
|
|
|
func DataLoadError(err error, dataType string) *AppError {
|
|
return NewAppError(
|
|
err,
|
|
"Error loading "+dataType+" data",
|
|
http.StatusInternalServerError,
|
|
true,
|
|
)
|
|
}
|
|
|
|
// ==============================================================================
|
|
// TYPED ERRORS
|
|
// Domain-specific error types for better error handling
|
|
// ==============================================================================
|
|
|
|
// ErrorCode represents a specific error condition
|
|
type ErrorCode string
|
|
|
|
const (
|
|
ErrCodeInvalidLanguage ErrorCode = "INVALID_LANGUAGE"
|
|
ErrCodeInvalidLength ErrorCode = "INVALID_LENGTH"
|
|
ErrCodeInvalidIcons ErrorCode = "INVALID_ICONS"
|
|
ErrCodeInvalidTheme ErrorCode = "INVALID_THEME"
|
|
ErrCodeInvalidVersion ErrorCode = "INVALID_VERSION"
|
|
ErrCodeTemplateNotFound ErrorCode = "TEMPLATE_NOT_FOUND"
|
|
ErrCodeTemplateRender ErrorCode = "TEMPLATE_RENDER"
|
|
ErrCodeDataLoad ErrorCode = "DATA_LOAD"
|
|
ErrCodePDFGeneration ErrorCode = "PDF_GENERATION"
|
|
ErrCodeMethodNotAllowed ErrorCode = "METHOD_NOT_ALLOWED"
|
|
ErrCodeUnauthorized ErrorCode = "UNAUTHORIZED"
|
|
ErrCodeForbidden ErrorCode = "FORBIDDEN"
|
|
ErrCodeRateLimitExceeded ErrorCode = "RATE_LIMIT_EXCEEDED"
|
|
)
|
|
|
|
// DomainError represents a domain-specific error
|
|
type DomainError struct {
|
|
Code ErrorCode
|
|
Message string
|
|
Err error
|
|
StatusCode int
|
|
Field string // Optional field that caused the error
|
|
}
|
|
|
|
// Error implements the error interface
|
|
func (e *DomainError) Error() string {
|
|
if e.Err != nil {
|
|
return fmt.Sprintf("%s: %v", e.Code, e.Err)
|
|
}
|
|
return fmt.Sprintf("%s: %s", e.Code, e.Message)
|
|
}
|
|
|
|
// Unwrap returns the underlying error
|
|
func (e *DomainError) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
|
|
// NewDomainError creates a new domain error
|
|
func NewDomainError(code ErrorCode, message string, statusCode int) *DomainError {
|
|
return &DomainError{
|
|
Code: code,
|
|
Message: message,
|
|
StatusCode: statusCode,
|
|
}
|
|
}
|
|
|
|
// WithError adds an underlying error
|
|
func (e *DomainError) WithError(err error) *DomainError {
|
|
e.Err = err
|
|
return e
|
|
}
|
|
|
|
// WithField adds field information
|
|
func (e *DomainError) WithField(field string) *DomainError {
|
|
e.Field = field
|
|
return e
|
|
}
|
|
|
|
// NOTE: Domain error constructors were removed as they were unused.
|
|
// If needed in the future, they can be re-added following the DomainError pattern above.
|
|
// See git history for the previous implementation.
|