package handlers import ( "fmt" "gestion/db" "log" "net/http" "strconv" "strings" "sync" "time" "github.com/gin-gonic/gin" ) var ( rateLimitMap = make(map[string][]time.Time) rateLimitMu sync.Mutex maxRequests = 10 timeWindow = time.Minute ) func checkRateLimit(key string) bool { rateLimitMu.Lock() defer rateLimitMu.Unlock() now := time.Now() if timestamps, exists := rateLimitMap[key]; exists { var validTimestamps []time.Time for _, ts := range timestamps { if now.Sub(ts) < timeWindow { validTimestamps = append(validTimestamps, ts) } } rateLimitMap[key] = validTimestamps if len(validTimestamps) >= maxRequests { return false } } rateLimitMap[key] = append(rateLimitMap[key], now) return true } // ============================================ // GESTION ADRESSE & ADMIN // ============================================ func safeGetUsername(c *gin.Context) (string, error) { username, exists := c.Get("username") if !exists { return "", fmt.Errorf("utilisateur non authentifié") } usernameStr, ok := username.(string) if !ok || usernameStr == "" { return "", fmt.Errorf("username invalide") } return usernameStr, nil } func validateAddress(address string) error { if len(address) == 0 { return fmt.Errorf("adresse vide") } if len(address) > 500 { return fmt.Errorf("adresse trop longue (max 500 caractères)") } if strings.TrimSpace(address) == "" { return fmt.Errorf("adresse invalide") } return nil } // UpdateCommandAddress met à jour l'adresse de livraison d'une commande // PUT /api/v1/admin/commands/:id/address func UpdateCommandAddress(c *gin.Context) { database := c.MustGet("database").(*db.Database) // ✅ Vérification du rôle if c.GetString("role") != "admin" { log.Printf("❌ [UPD_ADDR] Accès refusé - role=%s", c.GetString("role")) c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } // ✅ Récupération sécurisée du username adminUsername, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } // ✅ Rate limiting rateLimitKey := fmt.Sprintf("update_addr:%s", adminUsername) if !checkRateLimit(rateLimitKey) { log.Printf("⚠️ [UPD_ADDR] Rate limit dépassé pour %s", adminUsername) c.JSON(http.StatusTooManyRequests, gin.H{"error": "Trop de requêtes, réessayez plus tard"}) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil || commandID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } var req struct { DeliveryAddress string `json:"delivery_address" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) return } // ✅ Validation de l'adresse if err := validateAddress(req.DeliveryAddress); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // ✅ Logs sanitizés log.Printf("📝 [UPD_ADDR] Admin %s modifie cmd %d", adminUsername, commandID) command, err := database.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) return } status, _ := command["status"].(string) if status == "livre" || status == "approved" || status == "cancelled" { c.JSON(http.StatusBadRequest, gin.H{ "error": "Impossible de modifier l'adresse d'une commande terminée", }) return } if err := database.UpdateCommandAddress(commandID, req.DeliveryAddress); err != nil { log.Printf("❌ [UPD_ADDR] Erreur DB: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de la mise à jour", }) return } database.AddCommandLog(commandID, "address_updated", fmt.Sprintf("Adresse mise à jour par admin %s", adminUsername), adminUsername) log.Printf("✅ [UPD_ADDR] Commande %d mise à jour", commandID) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Adresse de livraison mise à jour", "command_id": commandID, }) } // ProposeAddressChange propose une nouvelle adresse au client pour validation // POST /api/v2/admin/protected/orders/:id/propose-address // POST /api/v1/cabine/commands/:id/propose-address func ProposeAddressChange(c *gin.Context) { database := c.MustGet("database").(*db.Database) userRole := c.GetString("role") if userRole != "admin" && userRole != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } staffUsername, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil || commandID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } var req struct { ProposedAddress string `json:"proposed_address" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) return } if err := validateAddress(req.ProposedAddress); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Récupérer la commande pour vérifier statut et obtenir username client command, err := database.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) return } status, _ := command["status"].(string) if status == "livre" || status == "approved" || status == "cancelled" { c.JSON(http.StatusBadRequest, gin.H{"error": "Impossible de modifier l'adresse d'une commande terminée"}) return } if err := database.ProposeAddressChange(commandID, req.ProposedAddress, staffUsername); err != nil { log.Printf("❌ [PROPOSE_ADDR] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Notifier le client clientUsername, _ := command["username"].(string) if clientUsername != "" { msg := fmt.Sprintf("📍 Une nouvelle adresse de livraison vous est proposée pour la commande #%d : %s. Veuillez l'accepter ou la refuser dans le suivi de commande.", commandID, req.ProposedAddress) database.NotifyClient(clientUsername, commandID, "address_proposal", msg) } log.Printf("✅ [PROPOSE_ADDR] Commande %d - nouvelle adresse proposée par %s", commandID, staffUsername) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Nouvelle adresse proposée au client", "command_id": commandID, }) } // RespondToAddressProposal permet au client d'accepter ou refuser une proposition d'adresse // POST /api/v1/commands/:id/address/respond func RespondToAddressProposal(c *gin.Context) { database := c.MustGet("database").(*db.Database) clientUsername, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil || commandID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } var req struct { Accepted bool `json:"accepted"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) return } if err := database.RespondToAddressProposal(commandID, clientUsername, req.Accepted); err != nil { log.Printf("❌ [RESPOND_ADDR] Erreur: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } action := "refusée" if req.Accepted { action = "acceptée" } log.Printf("✅ [RESPOND_ADDR] Commande %d - proposition %s par %s", commandID, action, clientUsername) c.JSON(http.StatusOK, gin.H{ "success": true, "message": fmt.Sprintf("Proposition d'adresse %s", action), }) } func GetAllCommands(c *gin.Context) { database := c.MustGet("database").(*db.Database) status := c.Query("status") username := c.Query("username") userRole := c.GetString("role") if userRole != "admin" && userRole != "cabine" { log.Printf("❌ [VALIDATE] Accès refusé - role=%s", userRole) c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } if username == "" { usernameParam := c.Param("username") if usernameParam != "" && usernameParam != "all" { username = usernameParam } } if status == "" { statusParam := c.Param("status") if statusParam != "" && statusParam != "all" { status = statusParam } } log.Printf("🔍 [GET_CMDS] Filtre - status=[%s], username=[%s]", status, username) commands, err := database.GetAllCommands(status, username) if err != nil { log.Printf("❌ [GET_CMDS] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de la récupération des commandes", "details": err.Error(), }) return } // ✅ MODIFICATION: Pour le count, utiliser len(commands) au lieu de GetCommandCount() // Cela permet d'avoir le vrai nombre de commandes retournées countCommand := len(commands) log.Printf("✅ [GET_CMDS] Trouvées: %d commandes", countCommand) c.JSON(http.StatusOK, gin.H{ "success": true, "commands": commands, "count": countCommand, }) } // GetCommandByID récupère une commande complète avec logs // GET /api/v1/commands/:id func GetCommandByID(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) return } userRole := c.GetString("role") commandID, err := strconv.Atoi(c.Param("id")) if err != nil || commandID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } log.Printf("📋 [GET_CMD] User %s (%s) récupère cmd %d", username, userRole, commandID) command, err := database.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) return } // ✅ CRITIQUE: Vérification de propriété cmdUsername, _ := command["username"].(string) // Seuls l'admin, la cabine ou le propriétaire peuvent voir la commande if userRole != "admin" && userRole != "cabine" && cmdUsername != username { log.Printf("❌ [GET_CMD] Accès refusé - User %s tente d'accéder à cmd de %s", username, cmdUsername) c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } // ✅ Logs uniquement pour admin/cabine if userRole == "admin" || userRole == "cabine" { logs, err := database.GetCommandLogs(commandID) if err == nil { command["logs"] = logs } } log.Printf("✅ [GET_CMD] Commande %d récupérée", commandID) c.JSON(http.StatusOK, gin.H{ "success": true, "command": command, }) } func ApproveDelivery(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) return } // ✅ Rate limiting rateLimitKey := fmt.Sprintf("approve:%s", username) if !checkRateLimit(rateLimitKey) { c.JSON(http.StatusTooManyRequests, gin.H{"error": "Trop de requêtes"}) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil || commandID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } log.Printf("✅ [APPROVE] Client %s approuve cmd %d", username, commandID) // ✅ TRANSACTION ATOMIQUE dans la DB pour éviter race condition // Cette fonction doit être créée dans le fichier db totalPoints, pointCategory, err := database.ApproveDeliveryAtomic(commandID, username) if err != nil { log.Printf("❌ [APPROVE] Erreur: %v", err) // ❌ Ne pas exposer les détails de l'erreur c.JSON(http.StatusBadRequest, gin.H{ "error": "Impossible d'approuver la livraison", }) return } log.Printf("✅ [APPROVE] %d points attribués à %s (catégorie: %s)", totalPoints, username, pointCategory) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Livraison confirmée", "command_id": commandID, "points_earned": totalPoints, "category": pointCategory, }) } // ============================================ // CONFIRMATION RÉCEPTION PAR STAFF (ADMIN / CABINE) // ============================================ func StaffApproveDelivery(c *gin.Context) { database := c.MustGet("database").(*db.Database) role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux admins et à la cabine"}) return } staffUsername, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil || commandID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } log.Printf("✅ [STAFF_APPROVE] %s (%s) confirme réception cmd %d", staffUsername, role, commandID) totalPoints, pointCategory, clientUsername, err := database.ApproveDeliveryAtomicByStaff(commandID, staffUsername) if err != nil { log.Printf("❌ [STAFF_APPROVE] Erreur: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Impossible de confirmer la réception: " + err.Error()}) return } log.Printf("✅ [STAFF_APPROVE] %d points attribués au client %s (catégorie: %s)", totalPoints, clientUsername, pointCategory) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Réception confirmée", "command_id": commandID, "client_username": clientUsername, "points_earned": totalPoints, "category": pointCategory, }) } // ============================================ // APPROBATION PAR ADMIN // ============================================ func ValidateDelivery(c *gin.Context) { database := c.MustGet("database").(*db.Database) if c.GetString("role") != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } adminUsername, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } // ✅ Rate limiting rateLimitKey := fmt.Sprintf("validate:%s", adminUsername) if !checkRateLimit(rateLimitKey) { c.JSON(http.StatusTooManyRequests, gin.H{"error": "Trop de requêtes"}) return } var req struct { CommandID int `json:"command_id"` CommandIDs []int `json:"command_ids"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) return } commandIDs := req.CommandIDs if req.CommandID > 0 && len(commandIDs) == 0 { commandIDs = []int{req.CommandID} } // ✅ LIMITE sur le nombre d'IDs const maxCommandIDs = 50 if len(commandIDs) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Aucun ID fourni"}) return } if len(commandIDs) > maxCommandIDs { c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Maximum %d commandes à la fois", maxCommandIDs), }) return } // ✅ Validation des IDs for _, id := range commandIDs { if id <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } } log.Printf("📝 [VALIDATE] Admin %s valide %d commande(s)", adminUsername, len(commandIDs)) var validated []gin.H var failed []gin.H for _, commandID := range commandIDs { command, err := database.GetCommandByID(commandID) if err != nil { failed = append(failed, gin.H{ "command_id": commandID, "error": "Commande non trouvée", }) continue } currentStatus, _ := command["status"].(string) validStatuses := []string{"assigned", "en_route", "pending", "livre"} isValid := false for _, s := range validStatuses { if currentStatus == s { isValid = true break } } if !isValid { failed = append(failed, gin.H{ "command_id": commandID, "error": "Statut invalide pour validation", }) continue } // ✅ Utiliser une transaction atomique pointsAwarded, err := database.ValidateDeliveryAtomic(commandID, adminUsername) if err != nil { log.Printf("❌ [VALIDATE] Erreur cmd %d: %v", commandID, err) failed = append(failed, gin.H{ "command_id": commandID, "error": "Erreur lors de la validation", }) continue } validated = append(validated, gin.H{ "command_id": commandID, "points_awarded": pointsAwarded, }) } log.Printf("✅ [VALIDATE] %d validées, %d échouées", len(validated), len(failed)) c.JSON(http.StatusOK, gin.H{ "success": true, "validated_count": len(validated), "failed_count": len(failed), "validated": validated, "failed": failed, }) } // ============================================ // GESTION ADMIN // ============================================ // GetAvailableDeliveryPersons récupère les livreurs disponibles // GET /api/v1/admin/delivery-persons/available func GetAvailableDeliveryPersons(c *gin.Context) { database := c.MustGet("database").(*db.Database) // ✅ SÉCURITÉ: Admin seulement userRole := c.GetString("role") if userRole != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } livreurs, err := database.GetAvailableDeliveryPersons() if err != nil { log.Printf("❌ [GET_LIVREURS] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de la récupération des livreurs", "details": err.Error(), }) return } log.Printf("✅ [GET_LIVREURS] Trouvés: %d livreurs disponibles", len(livreurs)) c.JSON(http.StatusOK, gin.H{ "success": true, "livreurs": livreurs, "count": len(livreurs), }) } // AssignDeliveryPerson assigne manuellement un livreur à une commande // Admin: POST /api/v2/admin/protected/delivery-persons/:username/assign/:command_id // Cabine: POST /api/v1/cabine/commands/:id/assign (body: {"livreur_username": "..."}) func AssignDeliveryPerson(c *gin.Context) { database := c.MustGet("database").(*db.Database) // ✅ SÉCURITÉ: Admin ou Cabine role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } // Support deux formats de route: :command_id (admin) ou :id (cabine) commandIDStr := c.Param("command_id") if commandIDStr == "" { commandIDStr = c.Param("id") } commandID, err := strconv.Atoi(commandIDStr) if err != nil || commandID == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } // Livreur depuis URL param (admin) ou body JSON (cabine) livreurUsername := c.Param("username") if livreurUsername == "" { var req struct { LivreurUsername string `json:"livreur_username" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Données invalides", "details": err.Error(), }) return } livreurUsername = req.LivreurUsername } log.Printf("👤 [ASSIGN] Assignation cmd %d à livreur %s", commandID, livreurUsername) staffUsername, _ := c.Get("username") if err := database.AssignDeliveryPerson(commandID, livreurUsername); err != nil { log.Printf("❌ [ASSIGN] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur assignation livreur", "details": err.Error(), }) return } database.AddCommandLog(commandID, "assigned", fmt.Sprintf("Livreur '%s' assigné manuellement par %s", livreurUsername, staffUsername), staffUsername.(string)) log.Printf("✅ [ASSIGN] Commande %d assignée à %s", commandID, livreurUsername) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Livreur assigné avec succès", "command_id": commandID, "livreur": livreurUsername, "assigned_by": staffUsername, }) } func GetClientCommandsHistory(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, exists := c.Get("username") if !exists { log.Printf("❌ [HISTORY] Utilisateur non authentifié") c.JSON(http.StatusUnauthorized, gin.H{"error": "Utilisateur non authentifié"}) return } usernameStr := username.(string) log.Printf("📚 [HISTORY] Récupération historique (approved) pour %s", usernameStr) commands, err := database.GetAllCommands("approved", usernameStr) if err != nil { log.Printf("❌ [HISTORY] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur récupération historique", "details": err.Error(), }) return } // ✅ AJOUTE CE LOG AVANT GetClientByUsername log.Printf("🔍 [HISTORY] AVANT GetClientByUsername pour: %s", usernameStr) client, err := database.GetClientByUsername(usernameStr) // ✅ AJOUTE CES LOGS APRÈS GetClientByUsername if err != nil { log.Printf("❌ [HISTORY] Erreur GetClientByUsername: %v", err) } else if client == nil { log.Printf("⚠️ [HISTORY] client est NIL!") } else { log.Printf("✅ [HISTORY] Client récupéré: username=%s, point=%d, point_zipette=%d", client.Username, client.Point, client.PointZipette) } resp := gin.H{ "success": true, "commands": commands, "count": len(commands), } if err == nil && client != nil { // ✅ VÉRIFIE AUSSI que client != nil resp["client_stats"] = gin.H{ "username": client.Username, "nom": client.Nom, "prenom": client.Prenom, "telephone": client.Telephone, "total_commands": client.Command, "points": client.Point, "points_zipette": client.PointZipette, "penalties": client.Amende, } log.Printf("✅ [HISTORY] Stats client: point=%d, point_zipette=%d", client.Point, client.PointZipette) } else { log.Printf("⚠️ [HISTORY] client_stats NON ajouté - err=%v, client=%v", err, client) } log.Printf("✅ [HISTORY] Historique: %d commandes approved", len(commands)) c.JSON(http.StatusOK, resp) } // ============================================ // NOTIFICATIONS CLIENT // ============================================ // NotifyClientToDescend envoie une notification push au client pour descendre récupérer sa commande // POST /api/v2/admin/protected/orders/:id/notify-client // POST /api/v1/cabine/commands/:id/notify-client func NotifyClientToDescend(c *gin.Context) { database := c.MustGet("database").(*db.Database) role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil || commandID == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } cmd, err := database.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande introuvable"}) return } clientUsername, ok := cmd["username"].(string) if !ok || clientUsername == "" { c.JSON(http.StatusInternalServerError, gin.H{"error": "Client introuvable"}) return } staffUsername, _ := c.Get("username") msg := fmt.Sprintf("Votre commande #%d est prête ! Vous pouvez descendre la récupérer.", commandID) database.NotifyClient(clientUsername, commandID, "ready_pickup", msg) database.AddCommandLog(commandID, "notification", fmt.Sprintf("Client notifié de descendre par %s", staffUsername), staffUsername.(string)) log.Printf("🔔 [NOTIFY] Client %s notifié pour commande %d par %s", clientUsername, commandID, staffUsername) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Client notifié", "client_username": clientUsername, }) } // ============================================ // DÉTAILS COMMANDES & ITEMS // ============================================ // ShowItems affiche les items d'une commande (Admin/Cabine) // GET /api/v1/admin/commands/:id/items func ShowItems(c *gin.Context) { database := c.MustGet("database").(*db.Database) userRole := c.GetString("role") // ✅ SÉCURITÉ: Admin ou Cabine seulement if userRole != "admin" && userRole != "cabine" { log.Printf("❌ [ITEMS] Accès refusé - role=%s", userRole) c.JSON(http.StatusForbidden, gin.H{ "error": "Accès refusé", "required_role": "admin ou cabine", "your_role": userRole, }) return } commandIDStr := c.Param("id") if commandIDStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande manquant"}) return } commandID, err := strconv.Atoi(commandIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } log.Printf("📦 [ITEMS] Récupération: cmd %d", commandID) items, err := database.GetCommandItems(commandID) if err != nil { log.Printf("❌ [ITEMS] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de la récupération des items", "details": err.Error(), }) return } if len(items) == 0 { log.Printf("⚠️ [ITEMS] Aucun item trouvé") c.JSON(http.StatusOK, gin.H{ "success": true, "items": []map[string]interface{}{}, "count": 0, }) return } log.Printf("✅ [ITEMS] %d items récupérés", len(items)) commandInfo := map[string]interface{}{ "id": items[0]["command_id"], "status": items[0]["command_status"], "address": items[0]["command_address"], "total_prix": items[0]["total_prix"], "referral_used": items[0]["referral_used"], "livreur": items[0]["livreur_assign"], "created_at": items[0]["command_created_at"], } clientInfo := map[string]interface{}{ "username": items[0]["client_username"], "nom": items[0]["client_nom"], "prenom": items[0]["client_prenom"], "telephone": items[0]["client_telephone"], } c.JSON(http.StatusOK, gin.H{ "success": true, "command_info": commandInfo, "client_info": clientInfo, "items": items, "count": len(items), }) } // GetCommandItemsWithDetails récupère les items enrichis // GET /api/v1/commands/:id/items/detailed func GetCommandItemsWithDetails(c *gin.Context) { database := c.MustGet("database").(*db.Database) // 🔐 Auth obligatoire username, exists := c.Get("username") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"}) return } userRole := c.GetString("role") usernameStr := username.(string) commandID, err := strconv.Atoi(c.Param("id")) if err != nil { log.Printf("❌ [ITEMS_DETAILED] ID invalide: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } log.Printf("📦 [ITEMS_DETAILED] User=%s Role=%s Cmd=%d", usernameStr, userRole, commandID) // 🔒 ÉTAPE 1 — Vérifier l'accès à la commande allowed, err := database.CanUserAccessCommand(commandID, usernameStr, userRole) if err != nil { log.Printf("❌ [ITEMS_DETAILED] Erreur vérif accès: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur vérification accès commande", }) return } if !allowed { log.Printf("🚨 [IDOR BLOCKED] User=%s Cmd=%d", usernameStr, commandID) c.JSON(http.StatusForbidden, gin.H{ "error": "Accès interdit à cette commande", }) return } // 🔓 ÉTAPE 2 — Accès autorisé, récupérer les items items, err := database.GetCommandItems(commandID) if err != nil { log.Printf("❌ [ITEMS_DETAILED] Erreur DB: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur récupération items", "details": err.Error(), }) return } if len(items) == 0 { c.JSON(http.StatusNotFound, gin.H{ "error": "Aucun item trouvé pour cette commande", }) return } commandInfo := map[string]interface{}{ "id": items[0]["command_id"], "command_status": items[0]["command_status"], "command_address": items[0]["command_address"], "total_prix": items[0]["total_prix"], "livreur_assign": items[0]["livreur_assign"], "command_created_at": items[0]["command_created_at"], } c.JSON(http.StatusOK, gin.H{ "success": true, "command_info": commandInfo, "items": items, "count": len(items), "total_price": commandInfo["total_prix"], "client_info": gin.H{ "username": items[0]["client_username"], "nom": items[0]["client_nom"], "prenom": items[0]["client_prenom"], "telephone": items[0]["client_telephone"], }, }) } // UpdateItemStatus met à jour le statut d'un item // PUT /api/v1/admin/items/:item_id/status func UpdateItemStatus(c *gin.Context) { database := c.MustGet("database").(*db.Database) itemID, err := strconv.Atoi(c.Param("item_id")) if err != nil { log.Printf("❌ [UPD_ITEM] Erreur conversion ID: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "ID d'item invalide"}) return } var req struct { Status string `json:"status" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { log.Printf("❌ [UPD_ITEM] Erreur JSON: %v", err) c.JSON(http.StatusBadRequest, gin.H{ "error": "Status requis", "details": err.Error(), }) return } log.Printf("📝 [UPD_ITEM] Mise à jour: item=%d, status=%s", itemID, req.Status) validStatuses := []string{"pending", "preparing", "delivered"} isValid := false for _, vs := range validStatuses { if req.Status == vs { isValid = true break } } if !isValid { log.Printf("❌ [UPD_ITEM] Statut invalide: %s", req.Status) c.JSON(http.StatusBadRequest, gin.H{ "error": "Statut invalide", "valid_statuses": validStatuses, "received_status": req.Status, }) return } err = database.UpdateCommandItemStatus(itemID, req.Status) if err != nil { log.Printf("❌ [UPD_ITEM] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de la mise à jour", "details": err.Error(), }) return } log.Printf("✅ [UPD_ITEM] Item %d mise à jour: %s", itemID, req.Status) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Statut de l'item mis à jour", "item_id": itemID, "new_status": req.Status, }) } // DeleteCommandItem supprime un item d'une commande // DELETE /api/v2/admin/protected/orders/:id/items/:item_id func DeleteCommandItem(c *gin.Context) { database := c.MustGet("database").(*db.Database) if c.GetString("role") != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } adminUsername, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil || commandID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } itemID, err := strconv.Atoi(c.Param("item_id")) if err != nil || itemID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID d'item invalide"}) return } log.Printf("🗑️ [DEL_ITEM] Admin %s supprime item %d de cmd %d", adminUsername, itemID, commandID) if err := database.DeleteCommandItem(commandID, itemID); err != nil { log.Printf("❌ [DEL_ITEM] Erreur: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } database.AddCommandLog(commandID, "item_deleted", fmt.Sprintf("Item %d supprimé par admin %s", itemID, adminUsername), adminUsername) log.Printf("✅ [DEL_ITEM] Item %d supprimé", itemID) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Item supprimé", "command_id": commandID, "item_id": itemID, }) } // UpdateCommandStatusAdmin met à jour le statut d'une commande (Admin) // PUT /api/v2/admin/protected/orders/:id/status func UpdateCommandStatusAdmin(c *gin.Context) { database := c.MustGet("database").(*db.Database) if c.GetString("role") != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil || commandID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } var req struct { Status string `json:"status" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Statut manquant"}) return } allowed := map[string]bool{ "pending": true, "assigned": true, "en_route": true, "livre": true, "approved": true, "cancelled": true, } if !allowed[req.Status] { c.JSON(http.StatusBadRequest, gin.H{"error": "Statut invalide: " + req.Status}) return } if err := database.UpdateCommandStatus(commandID, req.Status); err != nil { log.Printf("❌ [STATUS_ADMIN] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } log.Printf("✅ [STATUS_ADMIN] Cmd %d → %s", commandID, req.Status) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Statut mis à jour", "command_id": commandID, "new_status": req.Status, }) }