318 lines
8.9 KiB
Go
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)
|
|
}
|