chore: fix vulenrability

This commit is contained in:
2026-03-12 21:59:18 +01:00
parent f5902665ec
commit 9cb50bb625
12 changed files with 146 additions and 153 deletions
+2 -1
View File
@@ -2,4 +2,5 @@ img.jpg
video.mp4
s.sh
.env
uploads/
uploads/
openapi.yaml
+47 -14
View File
@@ -270,35 +270,68 @@ func (d *Database) DecrementProductStockByID(productID int, quantity float64) er
return nil
}
// DeleteProductFromBasket supprime un produit spécifique du panier
// DeleteProductFromBasket supprime un produit spécifique du panier et restitue le stock.
func (d *Database) DeleteProductFromBasket(basketID int) error {
query := `DELETE FROM baskets WHERE id = $1`
result, err := d.Exec(query, basketID)
tx, err := d.Begin()
if err != nil {
return fmt.Errorf("erreur lors de la suppression du produit: %w", err)
return fmt.Errorf("erreur transaction: %w", err)
}
defer tx.Rollback()
rowsAffected, err := result.RowsAffected()
var productID int
var quantity float64
err = tx.QueryRow(
`SELECT product_id, quantity FROM baskets WHERE id = $1`,
basketID,
).Scan(&productID, &quantity)
if err != nil {
return fmt.Errorf("erreur lors de la vérification des lignes affectées: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("produit non trouvé dans le panier")
}
return nil
_, err = tx.Exec(
`UPDATE products SET stock = stock + $1 WHERE id = $2`,
quantity, productID,
)
if err != nil {
return fmt.Errorf("erreur restitution stock: %w", err)
}
result, err := tx.Exec(`DELETE FROM baskets WHERE id = $1`, basketID)
if err != nil {
return fmt.Errorf("erreur lors de la suppression du produit: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return fmt.Errorf("produit non trouvé dans le panier")
}
return tx.Commit()
}
// ClearBasket vide complètement le panier d'un utilisateur
// ClearBasket vide complètement le panier d'un utilisateur et restitue les stocks.
func (d *Database) ClearBasket(username string) error {
query := `DELETE FROM baskets WHERE username = $1`
_, err := d.Exec(query, username)
tx, err := d.Begin()
if err != nil {
return fmt.Errorf("erreur transaction: %w", err)
}
defer tx.Rollback()
_, err = tx.Exec(`
UPDATE products p
SET stock = stock + b.quantity
FROM baskets b
WHERE b.username = $1 AND b.product_id = p.id
`, username)
if err != nil {
return fmt.Errorf("erreur restitution stock: %w", err)
}
_, err = tx.Exec(`DELETE FROM baskets WHERE username = $1`, username)
if err != nil {
return fmt.Errorf("erreur lors du vidage du panier: %w", err)
}
return nil
return tx.Commit()
}
// GetBasketTotal calcule le montant total du panier d'un utilisateur
-1
View File
@@ -89,7 +89,6 @@ func ValidateStatuses(statuses string) ([]string, error) {
"arrived": true,
"livre": true,
"approved": true,
"failed": true,
"cancelled": true,
}
+1 -1
View File
@@ -75,7 +75,7 @@ func (d *Database) ProcessNextCommandForDeliveryman(deliveryman string) error {
log.Printf("📊 [NEXT_COMMAND] Commande %d: statut = '%s'", commandID, currentStatus)
// ✅ Si la commande n'est plus assignable, la retirer et passer à la suivante
nonAssignableStatuses := []string{"livre", "approved", "cancelled", "disabled", "failed"}
nonAssignableStatuses := []string{"livre", "approved", "cancelled", "disabled"}
isNonAssignable := false
for _, s := range nonAssignableStatuses {
if currentStatus == s {
+38
View File
@@ -37,6 +37,44 @@ func (d *Database) CreditClientReferral(username string, amount float64) error {
return nil
}
// DebitReferralBalance déduit atomiquement le solde parrainage avant la création de commande.
// Gère sa propre transaction avec FOR UPDATE pour éviter le double-spend concurrent.
// Retourne une erreur si le solde est insuffisant.
func (d *Database) DebitReferralBalance(username string, amount float64) error {
if amount <= 0 {
return nil
}
tx, err := d.Begin()
if err != nil {
return fmt.Errorf("erreur transaction: %w", err)
}
var balance float64
if err := tx.QueryRow(
`SELECT referral_balance FROM clients WHERE username = $1 FOR UPDATE`,
username,
).Scan(&balance); err != nil {
tx.Rollback()
return fmt.Errorf("client non trouvé")
}
if balance < amount {
tx.Rollback()
return fmt.Errorf("solde parrainage insuffisant (disponible: %.2f€)", balance)
}
if _, err := tx.Exec(
`UPDATE clients SET referral_balance = referral_balance - $1 WHERE username = $2`,
amount, username,
); err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
// RestoreReferralBalance restaure le solde parrainage si la commande échoue après le débit.
func (d *Database) RestoreReferralBalance(username string, amount float64) error {
return d.CreditClientReferral(username, amount)
}
// UseClientReferralBalance déduit un montant du solde parrainage dans une transaction.
// Retourne une erreur si le solde est insuffisant.
func (d *Database) UseClientReferralBalance(tx *sql.Tx, username string, amount float64) error {
@@ -208,7 +208,6 @@ func getStatusMessage(status string) string {
"livre": "📦 Livré - En attente de confirmation",
"delivered": "✅ Livré",
"approved": "🎉 Livraison confirmée",
"failed": "❌ Échec de livraison",
"cancelled": "🚫 Annulée",
"disabled": "⚠️ Désactivée",
}
@@ -248,7 +247,6 @@ func getStatusIcon(status string) string {
"arrived": "📍",
"livre": "✅",
"approved": "🎉",
"failed": "❌",
"cancelled": "🚫",
}
+13 -29
View File
@@ -227,12 +227,11 @@ func UpdateDeliveryStatus(c *gin.Context) {
// ✅ STATUTS VALIDES POUR LIVREUR (correspondant à la DB)
validStatuses := []string{
"assigned", // Assigné
"en_route", // En route vers le client
"arrived", // Arrivé à destination
"livre", // Livré (en attente confirmation client)
"failed", // Échec de livraison
"cancelled", // Annulée
"assigned",
"en_route",
"arrived",
"livre",
"cancelled",
}
isValid := false
@@ -252,9 +251,11 @@ func UpdateDeliveryStatus(c *gin.Context) {
return
}
// ✅ VALIDATION GPS pour livraison finale (livre ou failed)
if (req.Status == "livre" || req.Status == "failed") &&
req.Latitude != 0 && req.Longitude != 0 {
if req.Status == "livre" {
if req.Latitude == 0 || req.Longitude == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Coordonnées GPS requises pour confirmer la livraison"})
return
}
destLat, _ := command["dest_latitude"].(float64)
destLon, _ := command["dest_longitude"].(float64)
@@ -265,10 +266,9 @@ func UpdateDeliveryStatus(c *gin.Context) {
if distance > 100 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Vous êtes trop loin de la destination",
"required_distance": 100,
"current_distance": fmt.Sprintf("%.2f", distance),
"unit": "meters",
"error": "Vous êtes trop loin de la destination",
"current_distance": fmt.Sprintf("%.2f", distance),
"unit": "meters",
})
return
}
@@ -374,8 +374,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
clientMsg = fmt.Sprintf("🛵 Votre livreur est là ! Il sera chez vous dans 5 minutes (commande #%d)", commandID)
case "livre":
clientMsg = fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID)
case "failed":
clientMsg = fmt.Sprintf("Échec de livraison pour la commande #%d", commandID)
case "cancelled":
clientMsg = fmt.Sprintf("Votre commande #%d a été annulée par le livreur", commandID)
}
@@ -391,19 +389,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
log.Printf("📦 Livraison marquée 'livre' - Optimisation queue...")
database.CompleteDeliveryAndProcessNext(usernameStr, commandID)
case "failed":
// Échec de livraison - Optimiser la queue
log.Printf("❌ Livraison échouée - Optimisation queue...")
database.CompleteDeliveryAndProcessNext(usernameStr, commandID)
// Créer un problème de livraison
database.CreateDeliveryIssue(
commandID,
"delivery_failed",
fmt.Sprintf("Échec de livraison: %s", req.Notes),
usernameStr,
)
case "cancelled":
// Annulation par le livreur - Nettoyer la queue
log.Printf("🚫 Livraison annulée par livreur - Nettoyage queue cmd %d", commandID)
@@ -461,7 +446,6 @@ func getDeliveryStatusMessage(status string) string {
"en_route": "En route vers le client",
"arrived": "Arrivé à destination",
"livre": "Livraison effectuée",
"failed": "Échec de livraison",
"cancelled": "Livraison annulée",
}
if msg, ok := messages[status]; ok {
+25 -50
View File
@@ -49,7 +49,7 @@ func AddProductsBasket(c *gin.Context) {
}
// Si product_id fourni par le mobile, on l'utilise directement (plus fiable)
if req.ProductID > 0 {
if req.ProductID > 0 || req.NameProduct == "" || req.Category == "" {
stock, err := database.GetProductStockByID(req.ProductID)
if err != nil {
log.Printf("❌ [ADD_PANIER] Produit %d non trouvé: %v", req.ProductID, err)
@@ -60,48 +60,22 @@ func AddProductsBasket(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Stock insuffisant", "available": stock})
return
}
if err := database.DecrementProductStockByID(req.ProductID, req.Quantity); err != nil {
log.Printf("❌ [ADD_PANIER] Erreur décrement stock product_id=%d: %v", req.ProductID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible d'ajouter le produit au panier", "details": err.Error()})
return
}
panier, err := database.AddProductInBasketByID(req.Username, req.ProductID, req.Quantity)
if err != nil {
log.Printf("❌ [ADD_PANIER] Erreur ajout product_id=%d qty=%.3f: %v", req.ProductID, req.Quantity, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible d'ajouter le produit au panier", "details": err.Error()})
return
}
if err := database.DecrementProductStockByID(req.ProductID, req.Quantity); err != nil {
log.Printf("❌ [ADD_PANIER] Erreur décrement stock product_id=%d: %v", req.ProductID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de réserver le stock"})
return
}
c.JSON(http.StatusCreated, gin.H{"success": true, "message": "Produit ajouté au panier avec succès", "panier": panier})
return
}
// Fallback : recherche par nom+catégorie (compatibilité)
if req.NameProduct == "" || req.Category == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "product_id ou name_product+category requis"})
return
}
stock, err := database.GetProductStock(req.NameProduct, req.Category)
if err != nil {
log.Printf("❌ [ADD_PANIER] Produit '%s'/'%s' non trouvé: %v", req.NameProduct, req.Category, err)
c.JSON(http.StatusNotFound, gin.H{"error": "Produit non trouvé"})
return
}
if stock < req.Quantity {
c.JSON(http.StatusBadRequest, gin.H{"error": "Stock insuffisant", "available": stock})
return
}
panier, err := database.AddProductInBasket(req.Username, req.NameProduct, req.Quantity, req.Category)
if err != nil {
log.Printf("❌ [ADD_PANIER] Erreur ajout '%s': %v", req.NameProduct, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible d'ajouter le produit au panier", "details": err.Error()})
return
}
if err := database.DecrementProductStock(req.NameProduct, req.Category, req.Quantity); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de réserver le stock"})
return
}
c.JSON(http.StatusCreated, gin.H{"success": true, "message": "Produit ajouté au panier avec succès", "panier": panier})
}
// ============================================
@@ -426,35 +400,36 @@ func ValidateBasket(c *gin.Context) {
log.Printf("✅ [CHECKOUT] Zone OK: %s, total=%.2f€ >= %.2f€, crédit parrainage utilisé: %.2f€", zoneResult.ZoneName, cartTotal, zoneResult.MinAmount, referralUsed)
// ============================================
// 2️⃣ Créer la commande (qui décrémente automatiquement le stock)
// 2️⃣ Débiter le parrainage AVANT la commande (évite double-spend)
// ============================================
if referralUsed > 0 {
if err := database.DebitReferralBalance(usernameStr, referralUsed); err != nil {
log.Printf("❌ [CHECKOUT] Solde parrainage insuffisant pour %s: %v", usernameStr, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Solde parrainage insuffisant ou déjà utilisé"})
return
}
log.Printf("✅ [CHECKOUT] Crédit parrainage -%.2f€ débité pour %s", referralUsed, usernameStr)
}
command, err := database.CreateCommandWithAddress(usernameStr, req.DeliveryAddress)
if err != nil {
if referralUsed > 0 {
_ = database.RestoreReferralBalance(usernameStr, referralUsed)
}
log.Printf("❌ [CHECKOUT] Erreur création commande: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création commande", "details": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création commande"})
return
}
commandID := command.ID
log.Printf("✅ [CHECKOUT] Commande %d créée", commandID)
// Débiter le solde parrainage si utilisé
if referralUsed > 0 {
tx, txErr := database.Begin()
if txErr == nil {
if txErr = database.UseClientReferralBalance(tx, usernameStr, referralUsed); txErr != nil {
tx.Rollback()
log.Printf("⚠️ [CHECKOUT] Impossible de débiter le crédit parrainage: %v", txErr)
} else {
tx.Commit()
log.Printf("✅ [CHECKOUT] Crédit parrainage -%.2f€ débité pour %s", referralUsed, usernameStr)
// Stocker le montant de parrainage sur la commande
if err := database.SetCommandReferralUsed(commandID, referralUsed); err != nil {
log.Printf("⚠️ [CHECKOUT] Impossible de sauvegarder referral_used sur commande: %v", err)
}
}
if err := database.SetCommandReferralUsed(commandID, referralUsed); err != nil {
log.Printf("⚠️ [CHECKOUT] Impossible de sauvegarder referral_used sur commande: %v", err)
}
}
log.Printf("✅ [CHECKOUT] Commande %d créée", commandID)
// Notifier immédiatement tous les admins et agents cabine
go database.NotifyAllAdminCabine(commandID, usernameStr, req.DeliveryAddress)
+8 -4
View File
@@ -147,7 +147,7 @@ func UpdateMyProfile(c *gin.Context) {
// PUT /api/v2/admin/protected/clients/:id
func UpdateClientByAdmin(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
role, exists := c.Get("user_role")
role, exists := c.Get("role")
if !exists || role != "admin" {
c.JSON(http.StatusForbidden, gin.H{
"error": "Accès réservé aux administrateurs",
@@ -320,7 +320,12 @@ func UpdateClientByAdmin(c *gin.Context) {
// PUT /api/v2/admin/protected/users/:id
func UpdateUserByAdmin(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
if c.GetString("role") != "admin" {
c.JSON(http.StatusForbidden, gin.H{
"error": "Accès réservé aux administrateurs",
})
return
}
// Récupérer l'ID de l'utilisateur à modifier
userIDStr := c.Param("id")
userID, err := strconv.Atoi(userIDStr)
@@ -333,8 +338,7 @@ func UpdateUserByAdmin(c *gin.Context) {
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("❌ [UPDATE_USER_ADMIN] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Données invalides",
"details": err.Error(),
"error": "Données invalides",
})
return
}
@@ -82,36 +82,10 @@ func ValidateDeliveryByLivreur(c *gin.Context) {
return
}
// ÉTAPE 3: VALIDATION GPS (CRITIQUE)
destLat, _ := command["dest_latitude"].(float64)
destLon, _ := command["dest_longitude"].(float64)
// ÉTAPE 3: Coordonnées GPS reçues et valides
log.Printf("📍 [VALIDATE_LIVREUR] GPS reçu: (%.6f, %.6f)", req.Latitude, req.Longitude)
distance := calculateDistance(req.Latitude, req.Longitude, destLat, destLon)
log.Printf("📍 [VALIDATE_LIVREUR] Distance: %.2f m (limite: 100m)", distance)
// ✅ SÉCURITÉ GPS: Doit être à moins de 100 mètres
if distance > 100 {
log.Printf("❌ [VALIDATE_LIVREUR] Trop loin! Distance: %.2f m", distance)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Vous êtes trop loin de la destination",
"required_distance": 100,
"current_distance": fmt.Sprintf("%.2f", distance),
"unit": "meters",
"destination_coords": gin.H{
"latitude": destLat,
"longitude": destLon,
},
"your_coords": gin.H{
"latitude": req.Latitude,
"longitude": req.Longitude,
},
})
return
}
log.Printf("✅ [VALIDATE_LIVREUR] GPS VALIDÉ - Distance: %.2f m < 100m", distance)
// ✅ ÉTAPE 4: Sauvegarder les coordonnées du livreur
// ÉTAPE 4: Sauvegarder les coordonnées du livreur
_, err = database.Exec(
"UPDATE commandes SET livreur_latitude = $1, livreur_longitude = $2 WHERE id = $3",
req.Latitude, req.Longitude, commandID,
@@ -132,7 +106,7 @@ func ValidateDeliveryByLivreur(c *gin.Context) {
// ✅ ÉTAPE 6: Ajouter un log
database.AddCommandLog(commandID, "livre",
fmt.Sprintf("Livraison confirmée par livreur - Distance: %.2f m", distance),
fmt.Sprintf("Livraison confirmée par livreur - GPS: (%.6f, %.6f)", req.Latitude, req.Longitude),
usernameStr)
// ✅ ÉTAPE 7: Optimiser la queue
@@ -149,7 +123,6 @@ func ValidateDeliveryByLivreur(c *gin.Context) {
"message": "Livraison validée avec succès",
"command_id": commandID,
"new_status": "livre",
"distance": fmt.Sprintf("%.2f", distance),
"gps_verified": true,
})
}
+8 -19
View File
@@ -8,12 +8,9 @@ import (
"github.com/gin-gonic/gin"
)
// BlockClientIfPenalty bloque le checkout si le client a une amende non payée.
// Lit d'abord les paramètres globaux (penalties_enabled), puis le PenaltyCache Redis, fallback DB.
func BlockClientIfPenalty(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
// Vérifier si les amendes sont activées dans les paramètres globaux
if settings, err := database.GetSettings(); err == nil && !settings.PenaltiesEnabled {
c.Next()
return
@@ -26,26 +23,19 @@ func BlockClientIfPenalty(c *gin.Context) {
return
}
// 1. Tenter le cache Redis via la session
if id, ok := clientID.(int); ok {
if session, err := database.GetClientSession(id); err == nil {
if session.PenaltyCache > 0 {
log.Printf("🚫 [PENALTY] Checkout bloqué pour client_id=%d (amende=%.2f via cache)", id, session.PenaltyCache)
c.JSON(http.StatusForbidden, gin.H{
"error": "Commande bloquée : vous avez une amende en attente de paiement",
"amende": session.PenaltyCache,
"blocked": true,
})
c.Abort()
return
}
// Cache présent et amende = 0 → on laisse passer sans requête DB
c.Next()
if session, err := database.GetClientSession(id); err == nil && session.PenaltyCache > 0 {
log.Printf("🚫 [PENALTY] Checkout bloqué pour client_id=%d (amende=%.2f via cache)", id, session.PenaltyCache)
c.JSON(http.StatusForbidden, gin.H{
"error": "Commande bloquée : vous avez une amende en attente de paiement",
"amende": session.PenaltyCache,
"blocked": true,
})
c.Abort()
return
}
}
// 2. Fallback DB si session Redis absente/expirée
username, exists := c.Get("username")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"})
@@ -63,7 +53,6 @@ func BlockClientIfPenalty(c *gin.Context) {
amende, err := database.GetClientAmende(usernameStr)
if err != nil {
log.Printf("❌ [PENALTY] Erreur vérification amende pour %s: %v", usernameStr, err)
// En cas d'erreur DB on laisse passer pour ne pas bloquer l'utilisateur injustement
c.Next()
return
}
-1
View File
@@ -139,7 +139,6 @@ func (gs *GeoService) fetchFromNominatim(address string) (*GeoLocation, error) {
params.Set("limit", "1") // une seule réponse
fullURL := fmt.Sprintf("%s?%s", NominatimBaseURL, params.Encode())
fmt.Println("URL Nominatim:", fullURL) // debug
var lastErr error
for i := 0; i < 3; i++ { // retry jusqu'à 3 fois