// ============================================ // handlers/profile_handlers.go - VERSION CORRIGÉE // ============================================ // Gestion des modifications de profils package handlers import ( "log" "net/http" "strconv" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "gestion/db" "gestion/models" "gestion/utils" ) // ============================================ // MODIFICATION PROFIL CLIENT (PAR LE CLIENT) // ============================================ // UpdateMyProfile permet à un client de modifier son propre profil // PUT /api/v1/profile/update func UpdateMyProfile(c *gin.Context) { clientID := c.GetInt("client_id") database := c.MustGet("database").(*db.Database) var req models.UpdateClientProfileRequest if err := c.ShouldBindJSON(&req); err != nil { log.Printf("❌ [UPDATE_MY_PROFILE] Erreur binding: %v", err) c.JSON(http.StatusBadRequest, gin.H{ "error": "Données invalides", }) return } // Récupérer le client actuel client, err := database.GetClientByID(clientID) if err != nil { log.Printf("❌ [UPDATE_MY_PROFILE] Client non trouvé: ID=%d", clientID) c.JSON(http.StatusNotFound, gin.H{"error": "Client non trouvé"}) return } // Vérifier si des modifications sont demandées hasChanges := false // Mise à jour du username if req.Username != "" { req.Username = utils.StripHTML(req.Username) } if req.Username != "" && req.Username != client.Username { // Vérifier que le nouveau username n'existe pas (clients et users) if existingClient, _ := database.GetClientByUsername(req.Username); existingClient != nil { log.Printf("❌ [UPDATE_MY_PROFILE] Username déjà utilisé: %s", req.Username) c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"}) return } if existingUser, _ := database.GetUserByUsername(req.Username); existingUser != nil { log.Printf("❌ [UPDATE_MY_PROFILE] Username réservé: %s", req.Username) c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"}) return } client.Username = req.Username hasChanges = true } // Mise à jour du mot de passe if req.Password != "" { if len(req.Password) < 8 { c.JSON(http.StatusBadRequest, gin.H{"error": "Le mot de passe doit contenir au moins 8 caractères"}) return } hashed, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { log.Printf("❌ [UPDATE_MY_PROFILE] Erreur bcrypt: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur traitement mot de passe"}) return } client.Password = string(hashed) hasChanges = true } // Mise à jour du nom if req.Nom != "" { req.Nom = utils.StripHTML(req.Nom) } if req.Nom != "" && req.Nom != client.Nom { if len(req.Nom) < 2 { c.JSON(http.StatusBadRequest, gin.H{"error": "Le nom doit contenir au moins 2 caractères"}) return } client.Nom = req.Nom hasChanges = true } // Mise à jour du prénom if req.Prenom != "" { req.Prenom = utils.StripHTML(req.Prenom) } if req.Prenom != "" && req.Prenom != client.Prenom { if len(req.Prenom) < 2 { c.JSON(http.StatusBadRequest, gin.H{"error": "Le prénom doit contenir au moins 2 caractères"}) return } client.Prenom = req.Prenom hasChanges = true } // Mise à jour du téléphone if req.Telephone != "" && req.Telephone != client.Telephone { if !utils.ValidatePhoneNumber(req.Telephone) { log.Printf("❌ [UPDATE_MY_PROFILE] Téléphone invalide: %s", req.Telephone) c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"}) return } normalizedPhone := utils.NormalizePhoneNumber(req.Telephone) // Vérifier que le téléphone n'est pas déjà utilisé if existingClient, _ := database.GetClientByTelephone(normalizedPhone); existingClient != nil && existingClient.ID != clientID { log.Printf("❌ [UPDATE_MY_PROFILE] Téléphone déjà utilisé: %s", normalizedPhone) c.JSON(http.StatusConflict, gin.H{"error": "Ce numéro de téléphone est déjà utilisé"}) return } client.Telephone = normalizedPhone hasChanges = true } if !hasChanges { c.JSON(http.StatusOK, gin.H{ "message": "Aucune modification détectée", "client": sanitizeClient(client), }) return } // Sauvegarder les modifications if err := database.UpdateClient(client); err != nil { log.Printf("❌ [UPDATE_MY_PROFILE] Erreur mise à jour: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la mise à jour"}) return } log.Printf("✅ [UPDATE_MY_PROFILE] Profil mis à jour: %s (ID=%d)", client.Username, client.ID) c.JSON(http.StatusOK, gin.H{ "message": "Profil mis à jour avec succès", "client": sanitizeClient(client), }) } // GetMyProfile retourne le profil du client connecté // GET /api/v1/profile func GetMyProfile(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, exists := c.Get("username") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"}) return } client, err := database.GetClientByUsername(username.(string)) if err != nil || client == nil { c.JSON(http.StatusNotFound, gin.H{"error": "Client non trouvé"}) return } c.JSON(http.StatusOK, gin.H{"success": true, "client": sanitizeClient(client)}) } // UpdateClientByAdmin permet à un admin de modifier n'importe quel profil client // PUT /api/v2/admin/protected/clients/:id func UpdateClientByAdmin(c *gin.Context) { database := c.MustGet("database").(*db.Database) role, exists := c.Get("role") if !exists || role != "admin" { c.JSON(http.StatusForbidden, gin.H{ "error": "Accès réservé aux administrateurs", }) return } // Récupérer l'ID du client à modifier clientIDStr := c.Param("id") clientID, err := strconv.Atoi(clientIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "ID client invalide"}) return } var req models.AdminUpdateClientRequest if err := c.ShouldBindJSON(&req); err != nil { log.Printf("❌ [UPDATE_CLIENT_ADMIN] Erreur binding: %v", err) c.JSON(http.StatusBadRequest, gin.H{ "error": "Données invalides", }) return } // Récupérer le client actuel client, err := database.GetClientByID(clientID) if err != nil { log.Printf("❌ [UPDATE_CLIENT_ADMIN] Client non trouvé: ID=%d", clientID) c.JSON(http.StatusNotFound, gin.H{"error": "Client non trouvé"}) return } // ✅ LOG DEBUG - État initial log.Printf("📊 [UPDATE_CLIENT_ADMIN] État initial - Command: %d, Amende: %.2f", client.Command, client.Amende) // Vérifier si des modifications sont demandées hasChanges := false // Mise à jour du username if req.Username != "" { req.Username = utils.StripHTML(req.Username) } if req.Username != "" && req.Username != client.Username { // Vérifier que le nouveau username n'existe pas if existingClient, _ := database.GetClientByUsername(req.Username); existingClient != nil { log.Printf("❌ [UPDATE_CLIENT_ADMIN] Username déjà utilisé: %s", req.Username) c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"}) return } client.Username = req.Username hasChanges = true log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Username modifié: %s", req.Username) } // Mise à jour du mot de passe (opération séparée pour garantir l'écriture) var newHashedPassword string if req.Password != "" { if len(req.Password) < 8 { c.JSON(http.StatusBadRequest, gin.H{"error": "Le mot de passe doit contenir au moins 8 caractères"}) return } hashed, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { log.Printf("❌ [UPDATE_CLIENT_ADMIN] Erreur bcrypt: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur traitement mot de passe"}) return } newHashedPassword = string(hashed) hasChanges = true log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Mot de passe modifié") } // Mise à jour du nom if req.Nom != "" { req.Nom = utils.StripHTML(req.Nom) } if req.Nom != "" && req.Nom != client.Nom { client.Nom = req.Nom hasChanges = true log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Nom modifié: %s", req.Nom) } // Mise à jour du prénom if req.Prenom != "" { req.Prenom = utils.StripHTML(req.Prenom) } if req.Prenom != "" && req.Prenom != client.Prenom { client.Prenom = req.Prenom hasChanges = true log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Prénom modifié: %s", req.Prenom) } // Mise à jour du téléphone if req.Telephone != "" && req.Telephone != client.Telephone { if !utils.ValidatePhoneNumber(req.Telephone) { log.Printf("❌ [UPDATE_CLIENT_ADMIN] Téléphone invalide: %s", req.Telephone) c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"}) return } normalizedPhone := utils.NormalizePhoneNumber(req.Telephone) // Vérifier que le téléphone n'est pas déjà utilisé if existingClient, _ := database.GetClientByTelephone(normalizedPhone); existingClient != nil && existingClient.ID != clientID { log.Printf("❌ [UPDATE_CLIENT_ADMIN] Téléphone déjà utilisé: %s", normalizedPhone) c.JSON(http.StatusConflict, gin.H{"error": "Ce numéro de téléphone est déjà utilisé"}) return } client.Telephone = normalizedPhone hasChanges = true log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Téléphone modifié: %s", normalizedPhone) } if req.Command != nil && *req.Command != client.Command { client.Command = *req.Command hasChanges = true log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Commandes modifiées: %d → %d", client.Command, *req.Command) } if req.Amende != nil { if *req.Amende < 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "L'amende ne peut pas être négative"}) return } if *req.Amende != client.Amende { client.Amende = *req.Amende hasChanges = true log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Amendes modifiées: %.2f → %.2f", client.Amende, *req.Amende) } } if !hasChanges { log.Printf("ℹ️ [UPDATE_CLIENT_ADMIN] Aucune modification détectée pour client ID=%d", clientID) c.JSON(http.StatusOK, gin.H{ "message": "Aucune modification détectée", "client": sanitizeClient(client), }) return } log.Printf("📊 [UPDATE_CLIENT_ADMIN] État avant save - Command: %d, Amende: %.2f", client.Command, client.Amende) // Sauvegarder les modifications de profil (hors mot de passe) if err := database.UpdateClient(client); err != nil { log.Printf("❌ [UPDATE_CLIENT_ADMIN] Erreur mise à jour profil: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la mise à jour"}) return } // Mettre à jour le mot de passe séparément si demandé if newHashedPassword != "" { if err := database.UpdateClientPasswordAndClearFlag(clientID, newHashedPassword); err != nil { log.Printf("❌ [UPDATE_CLIENT_ADMIN] Erreur mise à jour mot de passe: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la mise à jour du mot de passe"}) return } log.Printf("✅ [UPDATE_CLIENT_ADMIN] Mot de passe mis à jour pour client ID=%d", clientID) } log.Printf("✅ [UPDATE_CLIENT_ADMIN] Client mis à jour par admin: %s (ID=%d)", client.Username, client.ID) c.JSON(http.StatusOK, gin.H{ "message": "Client mis à jour avec succès", "client": sanitizeClient(client), }) } // UpdateUserByAdmin permet à un admin de modifier n'importe quel profil user // 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) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "ID utilisateur invalide"}) return } var req models.UpdateUserProfileRequest 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", }) return } // Récupérer l'utilisateur actuel user, err := database.GetUserByID(userID) if err != nil { log.Printf("❌ [UPDATE_USER_ADMIN] User non trouvé: ID=%d", userID) c.JSON(http.StatusNotFound, gin.H{"error": "Utilisateur non trouvé"}) return } // Vérifier si des modifications sont demandées hasChanges := false // Mise à jour du username if req.Username != "" { req.Username = utils.StripHTML(req.Username) } if req.Username != "" && req.Username != user.Username { // Vérifier que le nouveau username n'existe pas (users et clients) if existingUser, _ := database.GetUserByUsername(req.Username); existingUser != nil { log.Printf("❌ [UPDATE_USER_ADMIN] Username déjà utilisé: %s", req.Username) c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"}) return } if existingClient, _ := database.GetClientByUsername(req.Username); existingClient != nil { log.Printf("❌ [UPDATE_USER_ADMIN] Username réservé: %s", req.Username) c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"}) return } user.Username = req.Username hasChanges = true } // Mise à jour du mot de passe if req.Password != "" { if len(req.Password) < 8 { c.JSON(http.StatusBadRequest, gin.H{"error": "Le mot de passe doit contenir au moins 8 caractères"}) return } hashed, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { log.Printf("❌ [UPDATE_USER_ADMIN] Erreur bcrypt: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur traitement mot de passe"}) return } user.Password = string(hashed) hasChanges = true } // Mise à jour du rôle if req.Role != "" && req.Role != user.Role { // Valider le rôle validRoles := map[string]bool{ "admin": true, "cabine": true, "livreur": true, } if !validRoles[req.Role] { c.JSON(http.StatusBadRequest, gin.H{ "error": "Rôle invalide. Valeurs acceptées: admin, cabine, livreur", }) return } user.Role = req.Role hasChanges = true } if !hasChanges { c.JSON(http.StatusOK, gin.H{ "message": "Aucune modification détectée", "user": sanitizeUser(user), }) return } // Sauvegarder les modifications if err := database.UpdateUser(user); err != nil { log.Printf("❌ [UPDATE_USER_ADMIN] Erreur mise à jour: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la mise à jour"}) return } log.Printf("✅ [UPDATE_USER_ADMIN] User mis à jour par admin: %s (ID=%d, Role=%s)", user.Username, user.ID, user.Role) c.JSON(http.StatusOK, gin.H{ "message": "Utilisateur mis à jour avec succès", "user": sanitizeUser(user), }) } func sanitizeClient(client *models.Client) gin.H { return gin.H{ "id": client.ID, "username": client.Username, "nom": client.Nom, "prenom": client.Prenom, "telephone": client.Telephone, "command": client.Command, "points_extra": client.PointsExtra, "amende": client.Amende, } } func sanitizeUser(user *models.User) gin.H { return gin.H{ "id": user.ID, "username": user.Username, "role": user.Role, } }