chore: build

This commit is contained in:
2026-06-14 17:50:35 +02:00
parent 3c92a0371f
commit 3a0f725159
93 changed files with 5311 additions and 4224 deletions
+181 -55
View File
@@ -17,10 +17,6 @@ import (
"github.com/gin-gonic/gin"
)
// ============================================
// CONFIGURATION & LIMITES
// ============================================
const (
MaxFileSize = 10 * 1024 * 1024 // 10MB par fichier
MaxTotalUploadSize = 50 * 1024 * 1024 // 50MB total
@@ -30,7 +26,6 @@ const (
MaxProductsPerUser = 100 // Limite pour éviter spam
)
// ✅ MIME types autorisés (vérification réelle du contenu)
var allowedMimeTypes = map[string]bool{
"image/jpeg": true,
"image/png": true,
@@ -41,28 +36,6 @@ var allowedMimeTypes = map[string]bool{
"video/quicktime": true,
}
// ============================================
// MIDDLEWARE D'AUTHORIZATION
// ============================================
func RequireAdminOrCabine() gin.HandlerFunc {
return func(c *gin.Context) {
role := c.GetString("role")
if role != "admin" && role != "cabine" {
c.JSON(http.StatusForbidden, gin.H{
"error": "Accès refusé - Admin ou Cabine requis",
})
c.Abort()
return
}
c.Next()
}
}
// ============================================
// HELPERS DE VALIDATION
// ============================================
func validateProductName(name string) error {
if len(name) == 0 {
return fmt.Errorf("nom requis")
@@ -187,10 +160,6 @@ func sanitizeFilePath(path string) (string, error) {
return cleaned, nil
}
// ============================================
// CREATE PRODUCT - VERSION SÉCURISÉE
// ============================================
func CreateProduct(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -269,6 +238,7 @@ func CreateProduct(c *gin.Context) {
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)
@@ -294,9 +264,13 @@ func CreateProduct(c *gin.Context) {
return
}
activePriceStr := c.PostForm(activePriceKey)
activePrice := activePriceStr != "false"
prices = append(prices, models.ProductPrice{
Quantity: quantity,
Price: price,
Quantity: quantity,
Price: price,
ActivePrice: activePrice,
})
priceIndex++
@@ -309,6 +283,8 @@ func CreateProduct(c *gin.Context) {
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,
@@ -316,6 +292,7 @@ func CreateProduct(c *gin.Context) {
Description: description,
Stock: stock,
Unit: unit,
ComingSoon: comingSoon,
Prices: prices,
}
@@ -415,7 +392,7 @@ func CreateProduct(c *gin.Context) {
// ✅ CRÉER LE DOSSIER DE MANIÈRE SÉCURISÉE
destFolder := filepath.Join("uploads", mediaType+"s")
if err := os.MkdirAll(destFolder, 0755); err != nil {
if err := os.MkdirAll(destFolder, 0750); err != nil {
log.Printf("❌ [CreateProduct] Erreur création dossier: %v", err)
rollbackFiles(savedFiles)
database.DeleteProduct(product.ID)
@@ -475,10 +452,6 @@ func CreateProduct(c *gin.Context) {
})
}
// ============================================
// GET ENDPOINTS - SÉCURISÉS (lecture publique OK)
// ============================================
func GetAllProducts(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -491,7 +464,10 @@ func GetAllProducts(c *gin.Context) {
})
return
}
role := c.GetString("role")
if role != "admin" && role != "cabine" {
products = filterActivePrices(products)
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": products,
@@ -528,7 +504,10 @@ func GetProductsByCategory(c *gin.Context) {
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,
@@ -538,7 +517,6 @@ func GetProductsByCategory(c *gin.Context) {
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{
@@ -547,7 +525,6 @@ func GetProductByID(c *gin.Context) {
})
return
}
product, err := database.GetProductByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
@@ -556,21 +533,22 @@ func GetProductByID(c *gin.Context) {
})
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,
})
}
// ============================================
// UPDATE PRODUCT - VERSION SÉCURISÉE
// ============================================
func UpdateProduct(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -600,9 +578,10 @@ func UpdateProduct(c *gin.Context) {
Name string `json:"name"`
Category string `json:"category"`
Description string `json:"description"`
Stock float64 `json:"stock"`
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 {
@@ -629,12 +608,8 @@ func UpdateProduct(c *gin.Context) {
if updateData.Unit == "" {
updateData.Unit = "u"
}
if err := validateUnit(updateData.Unit); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := validateStock(updateData.Stock); err != nil {
if err := validateUnit(updateData.Unit); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
@@ -651,14 +626,32 @@ func UpdateProduct(c *gin.Context) {
}
}
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)
if err := database.UpdateProduct(id, updateData.Name, updateData.Category, updateData.Description, updateData.Unit, updateData.Stock, updateData.Prices); err != nil {
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)
@@ -672,6 +665,72 @@ func UpdateProduct(c *gin.Context) {
})
}
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)
@@ -803,7 +862,7 @@ func UploadMedia(c *gin.Context) {
// ✅ CRÉER LE DOSSIER
destFolder := filepath.Join("uploads", fileType+"s")
if err := os.MkdirAll(destFolder, 0755); err != nil {
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
@@ -849,6 +908,50 @@ func UploadMedia(c *gin.Context) {
})
}
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
// ============================================
@@ -947,3 +1050,26 @@ func cleanFileName(name string) string {
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
}