package handlers import ( "fmt" "gestion/db" "gestion/models" "gestion/utils" "log" "mime/multipart" "net/http" "os" "path/filepath" "strconv" "strings" "github.com/gabriel-vasile/mimetype" "github.com/gin-gonic/gin" ) const ( MaxFileSize = 10 * 1024 * 1024 // 10MB par fichier MaxTotalUploadSize = 50 * 1024 * 1024 // 50MB total MaxFilesPerProduct = 10 // Max 10 fichiers MaxNameLength = 200 MaxDescLength = 2000 MaxProductsPerUser = 100 // Limite pour éviter spam ) var allowedMimeTypes = map[string]bool{ "image/jpeg": true, "image/png": true, "image/gif": true, "image/webp": true, "video/mp4": true, "video/webm": true, "video/quicktime": true, } func validateProductName(name string) error { if len(name) == 0 { return fmt.Errorf("nom requis") } if len(name) > MaxNameLength { return fmt.Errorf("nom trop long (max %d caractères)", MaxNameLength) } // Sanitize if strings.Contains(name, "..") || strings.Contains(name, "/") { return fmt.Errorf("nom invalide") } return nil } func validateProductDescription(desc string) error { if len(desc) == 0 { return fmt.Errorf("description requise") } if len(desc) > MaxDescLength { return fmt.Errorf("description trop longue (max %d caractères)", MaxDescLength) } return nil } func validateStock(stock float64) error { if stock < 0 { return fmt.Errorf("stock ne peut pas être négatif") } if stock > 1000000 { return fmt.Errorf("stock trop élevé (max 1000000)") } return nil } func validatePrice(quantity float64, price float64) error { if quantity <= 0 { return fmt.Errorf("quantité doit être > 0") } if quantity > 10000 { return fmt.Errorf("quantité trop élevée (max 10000)") } if price <= 0 { return fmt.Errorf("prix doit être > 0") } if price > 100000 { return fmt.Errorf("prix trop élevé (max 100000)") } return nil } func validateUnit(unit string) error { validUnits := map[string]bool{ "u": true, // unité "kg": true, // kilogramme "g": true, // gramme "bag": true, // sac "l": true, // litre "cl": true, // centilitre "pcs": true, // pièces } if !validUnits[unit] { return fmt.Errorf("unité invalide : valeurs acceptées : u, kg, g, bag, l, cl, pcs") } return nil } func validateCategory(database *db.Database, category string) error { if category == "" { return fmt.Errorf("catégorie requise") } exists, err := database.CategoryExists(category) if err != nil { return fmt.Errorf("erreur vérification catégorie") } if !exists { return fmt.Errorf("catégorie invalide") } return nil } // ✅ VÉRIFICATION DU TYPE MIME RÉEL (pas juste l'extension) func validateFileMimeType(fileHeader *multipart.FileHeader) (string, error) { file, err := fileHeader.Open() if err != nil { return "", err } defer file.Close() mtype, err := mimetype.DetectReader(file) if err != nil { return "", err } mimeType := mtype.String() // Normaliser : couper les paramètres éventuels (ex: "video/mp4; codecs=...") if idx := strings.Index(mimeType, ";"); idx != -1 { mimeType = strings.TrimSpace(mimeType[:idx]) } if !allowedMimeTypes[mimeType] { return "", fmt.Errorf("type de fichier non autorisé: %s", mimeType) } return mimeType, nil } // ✅ PROTECTION CONTRE PATH TRAVERSAL func sanitizeFilePath(path string) (string, error) { // Nettoyer le chemin cleaned := filepath.Clean(path) // Vérifier qu'il ne contient pas de ".." if strings.Contains(cleaned, "..") { return "", fmt.Errorf("path traversal détecté") } // Vérifier qu'il commence par "uploads/" if !strings.HasPrefix(cleaned, "uploads/") && !strings.HasPrefix(cleaned, "uploads\\") { return "", fmt.Errorf("chemin invalide") } return cleaned, nil } func CreateProduct(c *gin.Context) { database := c.MustGet("database").(*db.Database) // ✅ VÉRIFIER LE RÔLE (déjà fait par middleware, double-check) role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } username, _ := safeGetUsername(c) // ✅ PARSER AVEC LIMITE DE TAILLE if err := c.Request.ParseMultipartForm(MaxTotalUploadSize); err != nil { log.Printf("❌ [CreateProduct] Formulaire trop grand: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Fichiers trop volumineux"}) return } // ✅ RÉCUPÉRER ET VALIDER LES DONNÉES name := strings.TrimSpace(c.PostForm("name")) category := strings.TrimSpace(c.PostForm("category")) description := strings.TrimSpace(c.PostForm("description")) stockStr := c.PostForm("stock") unit := strings.ToLower(strings.TrimSpace(c.PostForm("unit"))) if unit == "" { unit = "u" } // ✅ VALIDATION STRICTE if err := validateProductName(name); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := validateProductDescription(description); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // ✅ NETTOYER ET VALIDER LA CATÉGORIE category = strings.ToLower(strings.TrimSpace(category)) category = strings.Map(func(r rune) rune { if r < 32 || r == 127 { return -1 } return r }, category) if err := validateCategory(database, category); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := validateUnit(unit); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // ✅ VALIDER LE STOCK stock, err := strconv.ParseFloat(stockStr, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Stock invalide"}) return } if err := validateStock(stock); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // ✅ RÉCUPÉRER ET VALIDER LES PRIX prices := []models.ProductPrice{} priceIndex := 0 for priceIndex < 100 { // Limite anti-spam quantityKey := fmt.Sprintf("prices[%d][quantity]", priceIndex) priceKey := fmt.Sprintf("prices[%d][price]", priceIndex) activePriceKey := fmt.Sprintf("prices[%d][active_price]", priceIndex) quantityStr := c.PostForm(quantityKey) priceStr := c.PostForm(priceKey) if quantityStr == "" || priceStr == "" { break } quantity, err := strconv.ParseFloat(quantityStr, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Quantité invalide"}) return } price, err := strconv.ParseFloat(priceStr, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Prix invalide"}) return } if err := validatePrice(quantity, price); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } activePriceStr := c.PostForm(activePriceKey) activePrice := activePriceStr != "false" prices = append(prices, models.ProductPrice{ Quantity: quantity, Price: price, ActivePrice: activePrice, }) priceIndex++ } if len(prices) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Au moins un prix requis"}) return } log.Printf("✅ [CreateProduct] %s crée produit: %s", username, name) comingSoon := c.PostForm("coming_soon") == "true" // ✅ CRÉER LE PRODUIT product := models.Product{ Name: name, Category: category, Description: description, Stock: stock, Unit: unit, ComingSoon: comingSoon, Prices: prices, } err = database.CreateProduct(&product) if err != nil { log.Printf("❌ [CreateProduct] Erreur DB: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création produit"}) return } log.Printf("✅ [CreateProduct] Produit créé: ID=%d", product.ID) // ✅ TRAITER LES FICHIERS MÉDIAS AVEC SÉCURITÉ if c.Request.MultipartForm == nil || c.Request.MultipartForm.File == nil { c.JSON(http.StatusCreated, gin.H{ "success": true, "product": product, }) return } files, exists := c.Request.MultipartForm.File["media"] if !exists || len(files) == 0 { c.JSON(http.StatusCreated, gin.H{ "success": true, "product": product, }) return } // ✅ LIMITER LE NOMBRE DE FICHIERS if len(files) > MaxFilesPerProduct { database.DeleteProduct(product.ID) c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Maximum %d fichiers autorisés", MaxFilesPerProduct), }) return } log.Printf("📁 [CreateProduct] %d fichiers à traiter", len(files)) cleanProductName := cleanFileName(product.Name) uploadedMedia := []models.Media{} savedFiles := []string{} var totalSize int64 = 0 for i, fileHeader := range files { // ✅ VÉRIFIER LA TAILLE INDIVIDUELLE if fileHeader.Size > MaxFileSize { rollbackFiles(savedFiles) database.DeleteProduct(product.ID) c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Fichier %s trop volumineux (max %dMB)", fileHeader.Filename, MaxFileSize/(1024*1024)), }) return } totalSize += fileHeader.Size // ✅ VÉRIFIER LA TAILLE TOTALE if totalSize > MaxTotalUploadSize { rollbackFiles(savedFiles) database.DeleteProduct(product.ID) c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Taille totale dépassée (max %dMB)", MaxTotalUploadSize/(1024*1024)), }) return } log.Printf("📄 [%d/%d] Traitement: %s", i+1, len(files), fileHeader.Filename) // ✅ VÉRIFIER LE TYPE MIME RÉEL (pas juste l'extension) mimeType, err := validateFileMimeType(fileHeader) if err != nil { log.Printf("❌ [CreateProduct] Type MIME invalide: %v", err) rollbackFiles(savedFiles) database.DeleteProduct(product.ID) c.JSON(http.StatusBadRequest, gin.H{"error": "Type de fichier non autorisé"}) return } // ✅ DÉTERMINER LE TYPE DE MÉDIA var mediaType string if strings.HasPrefix(mimeType, "image/") { mediaType = "image" } else if strings.HasPrefix(mimeType, "video/") { mediaType = "video" } else { rollbackFiles(savedFiles) database.DeleteProduct(product.ID) c.JSON(http.StatusBadRequest, gin.H{"error": "Type de média non supporté"}) return } // ✅ GÉNÉRER UN NOM UNIQUE ET SÉCURISÉ uniqueFileName := utils.GenerateUniqueFileName(cleanProductName, fileHeader.Filename) // ✅ CRÉER LE DOSSIER DE MANIÈRE SÉCURISÉE destFolder := filepath.Join("uploads", mediaType+"s") if err := os.MkdirAll(destFolder, 0750); err != nil { log.Printf("❌ [CreateProduct] Erreur création dossier: %v", err) rollbackFiles(savedFiles) database.DeleteProduct(product.ID) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur système"}) return } filePath := filepath.Join(destFolder, uniqueFileName) // ✅ VALIDER LE CHEMIN (protection path traversal) safeFilePath, err := sanitizeFilePath(filePath) if err != nil { log.Printf("❌ [CreateProduct] Path traversal détecté: %v", err) rollbackFiles(savedFiles) database.DeleteProduct(product.ID) c.JSON(http.StatusBadRequest, gin.H{"error": "Chemin invalide"}) return } // ✅ SAUVEGARDER LE FICHIER if err := c.SaveUploadedFile(fileHeader, safeFilePath); err != nil { log.Printf("❌ [CreateProduct] Erreur sauvegarde: %v", err) rollbackFiles(savedFiles) database.DeleteProduct(product.ID) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur sauvegarde fichier"}) return } savedFiles = append(savedFiles, safeFilePath) // ✅ CRÉER L'ENTRÉE MÉDIA mediaURL := "/" + filepath.ToSlash(safeFilePath) media := models.Media{ ProductID: product.ID, Type: mediaType, URL: mediaURL, } if err := database.CreateMedia(&media); err != nil { log.Printf("❌ [CreateProduct] Erreur DB média: %v", err) rollbackFiles(savedFiles) database.DeleteProduct(product.ID) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création média"}) return } uploadedMedia = append(uploadedMedia, media) log.Printf("✅ [CreateProduct] Média %d créé", media.ID) } product.Media = uploadedMedia log.Printf("🎉 [CreateProduct] SUCCÈS: %d médias enregistrés", len(uploadedMedia)) c.JSON(http.StatusCreated, gin.H{ "success": true, "product": product, }) } func GetAllProducts(c *gin.Context) { database := c.MustGet("database").(*db.Database) products, err := database.GetAllProducts() if err != nil { log.Printf("❌ [GetAllProducts] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": "Erreur récupération produits", }) return } role := c.GetString("role") if role != "admin" && role != "cabine" { products = filterActivePrices(products) } c.JSON(http.StatusOK, gin.H{ "success": true, "data": products, "count": len(products), }) } func GetProductsByCategory(c *gin.Context) { database := c.MustGet("database").(*db.Database) category := strings.ToLower(strings.TrimSpace(c.Param("category"))) // ✅ VALIDATION if err := validateCategory(database, category); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": err.Error(), }) return } products, err := database.GetProductsByCategory(category) if err != nil { log.Printf("❌ [GetProductsByCategory] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": "Erreur récupération produits", }) return } // ✅ Charger les médias for i := range products { media, _ := database.GetMediaByProductID(products[i].ID) products[i].Media = media } roleCtx := c.GetString("role") if roleCtx != "admin" && roleCtx != "cabine" { products = filterActivePrices(products) } c.JSON(http.StatusOK, gin.H{ "success": true, "data": products, "count": len(products), }) } func GetProductByID(c *gin.Context) { database := c.MustGet("database").(*db.Database) id, err := strconv.Atoi(c.Param("id")) if err != nil || id <= 0 { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": "ID invalide", }) return } product, err := database.GetProductByID(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{ "success": false, "error": "Produit non trouvé", }) return } // ✅ Charger les médias media, _ := database.GetMediaByProductID(product.ID) product.Media = media // ✅ Filtrer les prix désactivés (sauf pour admin/cabine) role := c.GetString("role") if role != "admin" && role != "cabine" { filterActivepricesSingle(&product) } c.JSON(http.StatusOK, gin.H{ "success": true, "data": product, }) } func UpdateProduct(c *gin.Context) { database := c.MustGet("database").(*db.Database) // ✅ VÉRIFIER LE RÔLE role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } username, _ := safeGetUsername(c) id, err := strconv.Atoi(c.Param("id")) if err != nil || id <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } // ✅ VÉRIFIER QUE LE PRODUIT EXISTE _, err = database.GetProductByID(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Produit non trouvé"}) return } var updateData struct { Name string `json:"name"` Category string `json:"category"` Description string `json:"description"` Unit string `json:"unit"` Prices []models.ProductPrice `json:"prices"` Stock *float64 `json:"stock"` ComingSoon *bool `json:"coming_soon"` } if err := c.ShouldBindJSON(&updateData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) return } // ✅ VALIDATION COMPLÈTE if err := validateProductName(updateData.Name); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := validateProductDescription(updateData.Description); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := validateCategory(database, updateData.Category); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if updateData.Unit == "" { updateData.Unit = "u" } if err := validateUnit(updateData.Unit); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if len(updateData.Prices) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Au moins un prix requis"}) return } for _, price := range updateData.Prices { if err := validatePrice(price.Quantity, price.Price); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } } if updateData.Stock != nil { if err := validateStock(*updateData.Stock); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } } log.Printf("🔄 [UpdateProduct] %s met à jour produit #%d", username, id) comingSoon := false if updateData.ComingSoon != nil { comingSoon = *updateData.ComingSoon } if err := database.UpdateProduct(id, updateData.Name, updateData.Category, updateData.Description, updateData.Unit, comingSoon, updateData.Prices); err != nil { log.Printf("❌ [UpdateProduct] Erreur UPDATE: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur mise à jour"}) return } if updateData.Stock != nil { if err := database.SetProductStock(id, *updateData.Stock); err != nil { log.Printf("⚠️ [UpdateProduct] Erreur mise à jour stock: %v", err) } } // ✅ RÉCUPÉRER LE PRODUIT MIS À JOUR updatedProduct, _ := database.GetProductByID(id) media, _ := database.GetMediaByProductID(id) updatedProduct.Media = media log.Printf("✅ [UpdateProduct] Produit #%d mis à jour", id) c.JSON(http.StatusOK, gin.H{ "success": true, "product": updatedProduct, }) } func UpdateStock(c *gin.Context) { database := c.MustGet("database").(*db.Database) role := c.GetString("role") if role != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } username, _ := safeGetUsername(c) id, err := strconv.Atoi(c.Param("id")) if err != nil || id <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } // ✅ VÉRIFIER QUE LE PRODUIT EXISTE _, err = database.GetProductByID(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Produit non trouvé"}) return } var req struct { Stock float64 `json:"stock"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) return } if err := validateStock(req.Stock); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } reserved, err := database.GetReservedQuantityInBaskets(id) if err != nil { utils.ServerErr(c, "Erreur lecture réservations", err) return } if req.Stock+reserved < reserved { c.JSON(http.StatusBadRequest, gin.H{"error": "Stock invalide"}) return } log.Printf("🔄 [UpdateStock] %s met à jour le stock #%d (réservé en paniers: %.3f)", username, id, reserved) if err := database.SetProductStock(id, req.Stock); err != nil { log.Printf("❌ [UpdateProduct] Erreur UPDATE: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur mise à jour"}) return } updatedProduct, _ := database.GetProductByID(id) media, _ := database.GetMediaByProductID(id) updatedProduct.Media = media log.Printf("✅ [UpdateStock] le stock #%d est mis à jour", id) c.JSON(http.StatusOK, gin.H{ "success": true, "product": updatedProduct, "reserved_in_baskets": reserved, }) } func DeleteMedia(c *gin.Context) { database := c.MustGet("database").(*db.Database) // ✅ VÉRIFIER LE RÔLE role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } mediaID, err := strconv.Atoi(c.Param("media_id")) if err != nil || mediaID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } media, err := database.GetMediaByID(mediaID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Média non trouvé"}) return } // ✅ SÉCURISER LE CHEMIN AVANT SUPPRESSION filePath := strings.TrimPrefix(media.URL, "/") safeFilePath, err := sanitizeFilePath(filePath) if err != nil { log.Printf("❌ [DeleteMedia] Path invalide: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Chemin invalide"}) return } // ✅ SUPPRIMER LE FICHIER PHYSIQUE if err := os.Remove(safeFilePath); err != nil && !os.IsNotExist(err) { log.Printf("⚠️ [DeleteMedia] Erreur suppression fichier: %v", err) } // ✅ SUPPRIMER DE LA DB err = database.DeleteMedia(mediaID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression"}) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Média supprimé", }) } func UploadMedia(c *gin.Context) { database := c.MustGet("database").(*db.Database) // ✅ VÉRIFIER LE RÔLE username, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) return } role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } // ✅ RÉCUPÉRER ET VALIDER L'ID PRODUIT productID, err := strconv.Atoi(c.Param("id")) if err != nil || productID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID produit invalide"}) return } // ✅ VÉRIFIER QUE LE PRODUIT EXISTE productName, err := database.GetProductNameByID(productID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Produit non trouvé"}) return } log.Printf("📤 [UploadMedia] %s upload média pour produit #%d (%s)", username, productID, productName) // ✅ RÉCUPÉRER LE TYPE ET LE FICHIER fileType := c.PostForm("type") if fileType != "image" && fileType != "video" { c.JSON(http.StatusBadRequest, gin.H{"error": "Type invalide (image ou video requis)"}) return } file, err := c.FormFile("file") if err != nil { log.Printf("❌ [UploadMedia] Erreur récupération fichier: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Fichier manquant"}) return } // ✅ VÉRIFIER LA TAILLE const MaxFileSize = 10 * 1024 * 1024 // 10MB if file.Size > MaxFileSize { c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Fichier trop volumineux (max %dMB)", MaxFileSize/(1024*1024)), }) return } // ✅ VÉRIFIER LE TYPE MIME RÉEL detectedMime, err := validateFileMimeType(file) if err != nil { log.Printf("❌ [UploadMedia] Type MIME invalide: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Type de fichier non autorisé"}) return } log.Printf("📋 [UploadMedia] Type MIME détecté: %s", detectedMime) // Vérifier que le MIME correspond au type déclaré if fileType == "image" && !strings.HasPrefix(detectedMime, "image/") { c.JSON(http.StatusBadRequest, gin.H{"error": "Le fichier n'est pas une image valide"}) return } if fileType == "video" && !strings.HasPrefix(detectedMime, "video/") { c.JSON(http.StatusBadRequest, gin.H{"error": "Le fichier n'est pas une vidéo valide"}) return } // ✅ GÉNÉRER UN NOM UNIQUE cleanProductName := cleanFileName(productName) uniqueFileName := utils.GenerateUniqueFileName(cleanProductName, file.Filename) // ✅ CRÉER LE DOSSIER destFolder := filepath.Join("uploads", fileType+"s") if err := os.MkdirAll(destFolder, 0750); err != nil { log.Printf("❌ [UploadMedia] Erreur création dossier: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création dossier"}) return } // ✅ SAUVEGARDER LE FICHIER filePath := filepath.Join(destFolder, uniqueFileName) if err := c.SaveUploadedFile(file, filePath); err != nil { log.Printf("❌ [UploadMedia] Erreur sauvegarde: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur sauvegarde fichier"}) return } log.Printf("✅ [UploadMedia] Fichier sauvegardé: %s", filePath) // ✅ CRÉER L'ENTRÉE EN BASE mediaURL := "/" + filepath.ToSlash(filePath) media := models.Media{ ProductID: productID, Type: fileType, URL: mediaURL, } err = database.CreateMedia(&media) if err != nil { // Rollback: supprimer le fichier os.Remove(filePath) log.Printf("❌ [UploadMedia] Erreur DB: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création média"}) return } log.Printf("✅ [UploadMedia] Média #%d créé pour produit #%d", media.ID, productID) c.JSON(http.StatusCreated, gin.H{ "success": true, "message": "Média uploadé avec succès", "media": media, "uploaded_by": gin.H{ "username": username, "role": role, }, }) } func ActivePrice(c *gin.Context) { database := c.MustGet("database").(*db.Database) role := c.GetString("role") if role != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } id, err := strconv.Atoi(c.Param("id")) if err != nil || id <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } if err := database.AddActivePrice(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Prix activé avec succès"}) } func DesActivePrice(c *gin.Context) { database := c.MustGet("database").(*db.Database) role := c.GetString("role") if role != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } id, err := strconv.Atoi(c.Param("id")) if err != nil || id <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } if err := database.DeActivePrice(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Prix désactivé avec succès"}) } // ============================================ // DELETE PRODUCT - VERSION SÉCURISÉE // ============================================ func DeleteProduct(c *gin.Context) { database := c.MustGet("database").(*db.Database) // ✅ VÉRIFIER LE RÔLE role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } username, _ := safeGetUsername(c) id, err := strconv.Atoi(c.Param("id")) if err != nil || id <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } log.Printf("🗑️ [DeleteProduct] %s supprime produit #%d", username, id) // ✅ RÉCUPÉRER LES MÉDIAS AVANT SUPPRESSION mediaList, err := database.GetMediaByProductID(id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur récupération médias"}) return } // ✅ SUPPRIMER LES FICHIERS AVEC SÉCURITÉ for _, media := range mediaList { filePath := strings.TrimPrefix(media.URL, "/") safeFilePath, err := sanitizeFilePath(filePath) if err != nil { log.Printf("⚠️ [DeleteProduct] Path invalide: %v", err) continue } if err := os.Remove(safeFilePath); err != nil && !os.IsNotExist(err) { log.Printf("⚠️ [DeleteProduct] Erreur suppression: %v", err) } } // ✅ SUPPRIMER LES MÉDIAS DE LA DB database.DeleteMediaByProductID(id) // ✅ SUPPRIMER LE PRODUIT err = database.DeleteProduct(id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression produit"}) return } log.Printf("✅ [DeleteProduct] Produit #%d supprimé", id) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Produit supprimé", }) } // ============================================ // HELPERS // ============================================ func rollbackFiles(files []string) { for _, file := range files { safeFilePath, err := sanitizeFilePath(file) if err != nil { continue } os.Remove(safeFilePath) } } func cleanFileName(name string) string { replacements := map[string]string{ "/": "-", "\\": "-", ":": "-", "*": "-", "?": "-", "\"": "-", "<": "-", ">": "-", "|": "-", " ": "_", } result := name for old, new := range replacements { result = strings.ReplaceAll(result, old, new) } // Limiter la longueur if len(result) > 50 { result = result[:50] } return result } func filterActivePrices(products []models.Product) []models.Product { for i := range products { activePrices := []models.ProductPrice{} for _, p := range products[i].Prices { if p.ActivePrice { activePrices = append(activePrices, p) } } products[i].Prices = activePrices } return products } func filterActivepricesSingle(product *models.Product) { activePrices := []models.ProductPrice{} for _, p := range product.Prices { if p.ActivePrice { activePrices = append(activePrices, p) } } product.Prices = activePrices }