veza/veza-backend-api/internal/handlers/common.go
2025-12-03 20:29:37 +01:00

318 lines
8.9 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"time"
"veza-backend-api/internal/dto"
"veza-backend-api/internal/errors"
"veza-backend-api/internal/validators"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// ResponseData représente la structure standardisée des réponses API
type ResponseData struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Timestamp time.Time `json:"timestamp"`
RequestID string `json:"request_id,omitempty"`
}
// PaginationData représente les données de pagination
type PaginationData struct {
Page int `json:"page"`
Limit int `json:"limit"`
Total int64 `json:"total"`
TotalPages int `json:"total_pages"`
HasNext bool `json:"has_next"`
HasPrevious bool `json:"has_previous"`
NextCursor string `json:"next_cursor,omitempty"`
PreviousCursor string `json:"previous_cursor,omitempty"`
}
// PaginatedResponse représente une réponse paginée
type PaginatedResponse struct {
ResponseData
Pagination PaginationData `json:"pagination"`
}
// ValidationError et ValidationErrors sont maintenant dans internal/dto/validation.go
// pour éviter les cycles d'import. Utiliser dto.ValidationError et dto.ValidationErrors
// CommonHandler contient les dépendances communes aux handlers
type CommonHandler struct {
logger *zap.Logger
validator *validators.Validator // GO-013: Validator centralisé
}
// NewCommonHandler crée une nouvelle instance de CommonHandler
// GO-013: Initialise le validator centralisé
func NewCommonHandler(logger *zap.Logger) *CommonHandler {
return &CommonHandler{
logger: logger,
validator: validators.NewValidator(),
}
}
// ValidateRequest valide une requête avec le validator centralisé
// GO-013: Helper pour valider les requêtes et retourner des erreurs formatées
func (h *CommonHandler) ValidateRequest(c *gin.Context, req interface{}) bool {
validationErrors := h.validator.Validate(req)
if len(validationErrors) > 0 {
h.RespondWithValidationError(c, validationErrors)
return false
}
return true
}
// RespondWithSuccess répond avec une réponse de succès
func (h *CommonHandler) RespondWithSuccess(c *gin.Context, data interface{}, message string) {
response := ResponseData{
Success: true,
Message: message,
Data: data,
Timestamp: time.Now(),
RequestID: c.GetString("request_id"),
}
c.JSON(http.StatusOK, response)
}
// RespondWithError répond avec une erreur
func (h *CommonHandler) RespondWithError(c *gin.Context, statusCode int, message string, err error) {
response := ResponseData{
Success: false,
Error: message,
Timestamp: time.Now(),
RequestID: c.GetString("request_id"),
}
if err != nil {
h.logger.Error("Handler error",
zap.String("error", err.Error()),
zap.String("request_id", c.GetString("request_id")),
zap.String("endpoint", c.Request.URL.Path),
)
}
c.JSON(statusCode, response)
}
// RespondWithValidationError répond avec des erreurs de validation
// GO-013: Utilise dto.ValidationError pour éviter les cycles d'import
func (h *CommonHandler) RespondWithValidationError(c *gin.Context, errors []dto.ValidationError) {
response := ResponseData{
Success: false,
Error: "Validation failed",
Data: dto.ValidationErrors{Errors: errors},
Timestamp: time.Now(),
RequestID: c.GetString("request_id"),
}
c.JSON(http.StatusBadRequest, response)
}
// RespondWithPaginatedData répond avec des données paginées
func (h *CommonHandler) RespondWithPaginatedData(c *gin.Context, data interface{}, pagination PaginationData, message string) {
response := PaginatedResponse{
ResponseData: ResponseData{
Success: true,
Message: message,
Data: data,
Timestamp: time.Now(),
RequestID: c.GetString("request_id"),
},
Pagination: pagination,
}
c.JSON(http.StatusOK, response)
}
// BindJSON lie les données JSON de la requête à une structure
func (h *CommonHandler) BindJSON(c *gin.Context, obj interface{}) error {
if err := c.ShouldBindJSON(obj); err != nil {
h.logger.Warn("Failed to bind JSON",
zap.Error(err),
zap.String("request_id", c.GetString("request_id")),
)
return err
}
return nil
}
// GetUserIDFromContext extrait l'ID utilisateur du contexte
func (h *CommonHandler) GetUserIDFromContext(c *gin.Context) (string, error) {
userID, exists := c.Get("user_id")
if !exists {
return "", errors.NewUnauthorizedError("User not authenticated")
}
userIDStr, ok := userID.(string)
if !ok {
return "", errors.New(errors.ErrCodeValidation, "Invalid user ID type")
}
return userIDStr, nil
}
// GetPaginationParams extrait les paramètres de pagination de la requête
func (h *CommonHandler) GetPaginationParams(c *gin.Context) (page, limit int, cursor string) {
page = 1
limit = 20
if pageStr := c.Query("page"); pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
page = p
}
}
if limitStr := c.Query("limit"); limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
limit = l
}
}
cursor = c.Query("cursor")
return page, limit, cursor
}
// ValidatePagination valide les paramètres de pagination
// GO-013: Utilise dto.ValidationError
func (h *CommonHandler) ValidatePagination(page, limit int) []dto.ValidationError {
var validationErrors []dto.ValidationError
if page < 1 {
validationErrors = append(validationErrors, dto.ValidationError{
Field: "page",
Message: "Page must be greater than 0",
Value: strconv.Itoa(page),
})
}
if limit < 1 || limit > 100 {
validationErrors = append(validationErrors, dto.ValidationError{
Field: "limit",
Message: "Limit must be between 1 and 100",
Value: strconv.Itoa(limit),
})
}
return validationErrors
}
// LogRequest log une requête entrante
func (h *CommonHandler) LogRequest(c *gin.Context, operation string) {
h.logger.Info("Request received",
zap.String("method", c.Request.Method),
zap.String("path", c.Request.URL.Path),
zap.String("operation", operation),
zap.String("user_id", c.GetString("user_id")),
zap.String("request_id", c.GetString("request_id")),
zap.String("ip", c.ClientIP()),
zap.String("user_agent", c.Request.UserAgent()),
)
}
// LogResponse log une réponse sortante
func (h *CommonHandler) LogResponse(c *gin.Context, statusCode int, duration time.Duration) {
h.logger.Info("Response sent",
zap.Int("status_code", statusCode),
zap.Duration("duration", duration),
zap.String("request_id", c.GetString("request_id")),
)
}
// SetRequestID middleware pour ajouter un ID de requête
func (h *CommonHandler) SetRequestID() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = generateRequestID()
}
c.Set("request_id", requestID)
c.Header("X-Request-ID", requestID)
c.Next()
}
}
// generateRequestID génère un ID de requête unique
func generateRequestID() string {
return strconv.FormatInt(time.Now().UnixNano(), 36)
}
// ValidateRequiredFields valide que les champs requis sont présents
// GO-013: Utilise dto.ValidationError
func (h *CommonHandler) ValidateRequiredFields(fields map[string]interface{}) []dto.ValidationError {
var validationErrors []dto.ValidationError
for field, value := range fields {
if value == nil || value == "" {
validationErrors = append(validationErrors, dto.ValidationError{
Field: field,
Message: "This field is required",
})
}
}
return validationErrors
}
// SanitizeString nettoie une chaîne de caractères
func (h *CommonHandler) SanitizeString(input string) string {
// Supprimer les caractères de contrôle et les espaces en début/fin
cleaned := strings.TrimSpace(input)
// Limiter la longueur
if len(cleaned) > 1000 {
cleaned = cleaned[:1000]
}
return cleaned
}
// ParseJSON parse du JSON de manière sécurisée
func (h *CommonHandler) ParseJSON(data []byte, v interface{}) error {
if err := json.Unmarshal(data, v); err != nil {
h.logger.Error("Failed to parse JSON", zap.Error(err))
return err
}
return nil
}
// MarshalJSON sérialise en JSON de manière sécurisée
func (h *CommonHandler) MarshalJSON(v interface{}) ([]byte, error) {
data, err := json.Marshal(v)
if err != nil {
h.logger.Error("Failed to marshal JSON", zap.Error(err))
return nil, err
}
return data, nil
}
// GetClientIP obtient l'IP réelle du client
func (h *CommonHandler) GetClientIP(c *gin.Context) string {
// Vérifier les headers de proxy
if ip := c.GetHeader("X-Forwarded-For"); ip != "" {
return strings.Split(ip, ",")[0]
}
if ip := c.GetHeader("X-Real-IP"); ip != "" {
return ip
}
return c.ClientIP()
}
// RateLimitKey génère une clé pour le rate limiting
func (h *CommonHandler) RateLimitKey(c *gin.Context, prefix string) string {
userID := c.GetString("user_id")
if userID != "" {
return prefix + ":user:" + userID
}
return prefix + ":ip:" + h.GetClientIP(c)
}