package handlers import ( "bytes" "cangui/whatsapp/backend/jwt" "cangui/whatsapp/backend/models" "encoding/json" "fmt" "io" "log" "net/http" "strconv" "time" "github.com/gorilla/mux" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) // Simuler un SSO + Redirection selon rôle func LoginHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") var reqUser models.User if err := json.NewDecoder(r.Body).Decode(&reqUser); err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } var user models.User result := db.Where("email = ?", reqUser.Email).First(&user) if result.Error != nil { http.Error(w, "Invalid credentials", http.StatusUnauthorized) return } // Vérification du mot de passe hashé if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(reqUser.Password)); err != nil { http.Error(w, "Invalid credentials", http.StatusUnauthorized) return } // Création du JWT fmt.Printf("login"); fmt.Printf(user.SSOID) tokenString, err := jwt.CreateToken(user.SSOID) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } // Cookie HTTP-only http.SetCookie(w, &http.Cookie{ Name: "token", Value: tokenString, Path: "/", HttpOnly: true, Secure: false, // à mettre à true en prod HTTPS SameSite: http.SameSiteLaxMode, }) // HX-Redirect pour HTMX w.Header().Set("HX-Redirect", "/dashboard") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"message":"Login success"}`)) } } func SSOLoginHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { code := r.URL.Query().Get("code") var user models.User if result := db.Where("sso_id = ?", code).First(&user); result.Error != nil || !user.IsActive { http.Error(w, "Invalid SSO code", http.StatusUnauthorized) return } token, err := jwt.CreateToken(user.Email) if err != nil { http.Error(w, "Token error", http.StatusInternalServerError) return } http.SetCookie(w, &http.Cookie{ Name: "token", Value: token, Path: "/", HttpOnly: true, Secure: false, }) http.Redirect(w, r, "/dashboard", http.StatusSeeOther) } } func SSOLoginPostHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var body struct { Code string `json:"code"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Code == "" { http.Error(w, "Code SSO requis", http.StatusBadRequest) return } var user models.User if result := db.Where("sso_id = ?", body.Code).First(&user); result.Error != nil || !user.IsActive { http.Error(w, "SSO invalide ou utilisateur inactif", http.StatusUnauthorized) return } token, err := jwt.CreateToken(user.SSOID) if err != nil { http.Error(w, "Erreur génération token", http.StatusInternalServerError) return } http.SetCookie(w, &http.Cookie{ Name: "token", Value: token, Path: "/", HttpOnly: true, Secure: false, SameSite: http.SameSiteLaxMode, }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "message": "Connexion réussie", "token": token, }) } } var MessageTypeCreditCost = map[string]uint{ "text": 1, "image": 2, "video": 2, "audio": 1, "document": 2, "sticker": 1, "interactive": 1, "reaction": 0, "location": 1, "contacts": 1, } func WebhookHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var body map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } entry := body["entry"].([]interface{})[0].(map[string]interface{}) changes := entry["changes"].([]interface{})[0].(map[string]interface{}) value := changes["value"].(map[string]interface{}) // Gestion des messages entrants if msgs, ok := value["messages"]; ok { messages := msgs.([]interface{}) for _, msg := range messages { m := msg.(map[string]interface{}) waID := value["contacts"].([]interface{})[0].(map[string]interface{})["wa_id"].(string) msgID := m["id"].(string) typeMsg := m["type"].(string) content := extractMessageContent(m) var user models.User if err := db.Where("sso_id = ?", waID).First(&user).Error; err != nil { fmt.Println("Utilisateur non trouvé pour:", waID) continue } db.Create(&models.Conversation{ UserID: user.ID, From: waID, To: value["metadata"].(map[string]interface{})["display_phone_number"].(string), MessageID: msgID, Type: typeMsg, Content: content, Direction: "Entrant", }) credit := MessageTypeCreditCost[typeMsg] if credit > 0 { ConsumeCredits(db, &user, typeMsg, content, credit) } } } // Gestion des statuts (delivered, read, etc.) if statuses, ok := value["statuses"]; ok { for _, s := range statuses.([]interface{}) { status := s.(map[string]interface{}) msgID := status["id"].(string) state := status["status"].(string) db.Model(&models.Conversation{}).Where("message_id = ?", msgID).Update("status", state) } } w.WriteHeader(http.StatusOK) } } func extractMessageContent(m map[string]interface{}) string { t := m["type"].(string) switch t { case "text": return m["Message reçu"].(map[string]interface{})["body"].(string) case "image", "video", "audio", "document", "sticker": return m[t].(map[string]interface{})["id"].(string) case "interactive": return m[t].(map[string]interface{})["type"].(string) default: return "[non textuel]" } } func ConsumeCredits(db *gorm.DB, user *models.User, messageType, content string, credits uint) { user.CurrentMonthCredits -= credits db.Save(user) db.Create(&models.Consumption{ UserID: user.ID, MessageType: messageType, Description: content, CreditsUsed: credits, }) month := time.Now().Format("2006-01") var mc models.MonthlyConsumption db.Where("user_id = ? AND month = ?", user.ID, month).FirstOrInit(&mc) mc.UserID = user.ID mc.Month = month mc.TotalUsed += credits db.Save(&mc) } func GetUserConversations(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID, err := strconv.Atoi(mux.Vars(r)["id"]) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } var conversations []models.Conversation if err := db.Where("user_id = ?", userID). Order("created_at desc"). Find(&conversations).Error; err != nil { http.Error(w, "Error retrieving conversations", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(conversations) } } // WebhookVerifyHandler répond au GET initial de vérification Meta func WebhookVerifyHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { mode := r.URL.Query().Get("hub.mode") token := r.URL.Query().Get("hub.verify_token") challenge := r.URL.Query().Get("hub.challenge") if mode == "subscribe" && token == "secrettoken" { w.WriteHeader(http.StatusOK) w.Write([]byte(challenge)) return } w.WriteHeader(http.StatusForbidden) w.Write([]byte("Invalid verify token")) } } func WebhookReceiveHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var body map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } fmt.Println("\n📩 Webhook reçu :", body) entry := body["entry"].([]interface{})[0].(map[string]interface{}) change := entry["changes"].([]interface{})[0].(map[string]interface{}) value := change["value"].(map[string]interface{}) recipientID := value["metadata"].(map[string]interface{})["phone_number_id"].(string) var user models.User if err := db.Where("whatsapp_phone_number_id = ?", recipientID).First(&user).Error; err != nil { log.Println("❌ Utilisateur introuvable pour phone_number_id:", recipientID) w.WriteHeader(http.StatusOK) return } // 📥 Gestion des messages entrants if messages, ok := value["messages"]; ok { for _, m := range messages.([]interface{}) { msg := m.(map[string]interface{}) from := msg["from"].(string) msgType := msg["type"].(string) msgID := msg["id"].(string) content := extractMessageContent(msg) var parentID *uint if ctx, ok := msg["context"].(map[string]interface{}); ok { contextID := fmt.Sprintf("%v", ctx["id"]) var parent models.Conversation if err := db.Where("message_id = ?", contextID).First(&parent).Error; err == nil { parentID = &parent.ID content += fmt.Sprintf(" (réponse à %s)", contextID) } } conv := models.Conversation{ UserID: user.ID, From: from, To: recipientID, MessageID: msgID, Type: msgType, Content: content, Direction: "Entrant", Status: "Reçu", ParentID: parentID, } if err := db.Create(&conv).Error; err != nil { log.Println("❌ Erreur enregistrement conversation:", err) } } } // 📦 Gestion des statuts : sent, delivered, read, failed if statuses, ok := value["statuses"]; ok { for _, s := range statuses.([]interface{}) { status := s.(map[string]interface{}) msgID := status["id"].(string) state := status["status"].(string) timestamp := status["timestamp"].(string) log.Printf("📦 Statut reçu : %s pour %s à %s", state, msgID, timestamp) update := map[string]interface{}{ "status": state, } if state == "read" { parsed, err := strconv.ParseInt(timestamp, 10, 64) if err == nil { update["read_at"] = time.Unix(parsed, 0) } } db.Model(&models.Conversation{}). Where("message_id = ?", msgID). Updates(update) } } w.WriteHeader(http.StatusOK) } } func CreateUser(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var user models.User if err := json.NewDecoder(r.Body).Decode(&user); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) if err != nil { http.Error(w, "Hash error", http.StatusInternalServerError) return } user.Password = string(hash) if err := db.Create(&user).Error; err != nil { http.Error(w, "Create failed", http.StatusInternalServerError) return } json.NewEncoder(w).Encode(user) } } func GetAllUsers(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var users []models.User db.Find(&users) json.NewEncoder(w).Encode(users) } } func GetUserByID(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id, _ := strconv.Atoi(mux.Vars(r)["id"]) var user models.User if err := db.First(&user, id).Error; err != nil { http.Error(w, "Not found", http.StatusNotFound) return } json.NewEncoder(w).Encode(user) } } func UpdateUser(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id, _ := strconv.Atoi(mux.Vars(r)["id"]) var user models.User if err := db.First(&user, id).Error; err != nil { http.Error(w, "Not found", http.StatusNotFound) return } var input models.User if err := json.NewDecoder(r.Body).Decode(&input); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } if input.Password != "" { hash, _ := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) input.Password = string(hash) } else { input.Password = user.Password } input.ID = user.ID if err := db.Model(&user).Updates(input).Error; err != nil { http.Error(w, "Update failed", http.StatusInternalServerError) return } json.NewEncoder(w).Encode(user) } } func DeleteUser(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id, _ := strconv.Atoi(mux.Vars(r)["id"]) if err := db.Delete(&models.User{}, id).Error; err != nil { http.Error(w, "Delete failed", http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } } func HandleTemplateTest(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { val := r.Context().Value("ssoid") ssoid, ok := val.(string) if !ok || ssoid == "" { http.Error(w, "Non authentifié", http.StatusUnauthorized) return } if err := r.ParseForm(); err != nil { http.Error(w, "Form invalide", http.StatusBadRequest) return } to := r.FormValue("to") templateName := r.FormValue("template_name") lang := r.FormValue("language") var params []string for i := 1; i <= 5; i++ { val := r.FormValue(fmt.Sprintf("param%d", i)) if val != "" { params = append(params, val) } } var user models.User if err := db.Where("sso_id = ?", ssoid).First(&user).Error; err != nil || user.ID == 0 { http.Error(w, "Utilisateur introuvable", http.StatusUnauthorized) return } message := models.NewTemplateMessage(to, templateName, lang, params) jsonBody, _ := json.MarshalIndent(message, "", " ") apiURL := fmt.Sprintf("https://graph.facebook.com/v22.0/%s/messages", user.WhatsappPhoneNumberID) req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonBody)) if err != nil { http.Error(w, "Requête WhatsApp échouée", http.StatusInternalServerError) return } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+user.WhatsappToken) resp, err := http.DefaultClient.Do(req) if err != nil { http.Error(w, "Échec de l'appel API WhatsApp", http.StatusBadGateway) return } defer resp.Body.Close() var respBody map[string]interface{} json.NewDecoder(resp.Body).Decode(&respBody) messageID := "" if messages, ok := respBody["messages"].([]interface{}); ok && len(messages) > 0 { if msgMap, ok := messages[0].(map[string]interface{}); ok { messageID = fmt.Sprintf("%v", msgMap["id"]) } } description := fmt.Sprintf("Template: %s, Lang: %s, Params: %v", templateName, lang, params) db.Create(&models.Conversation{ UserID: user.ID, From: user.NameClient, To: to, MessageID: messageID, Type: "template", Content: description, Direction: "Sortant", Status: "Envoyé", }) w.WriteHeader(resp.StatusCode) json.NewEncoder(w).Encode(respBody) } } func SendWhatsAppMessage(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { val := r.Context().Value("ssoid") ssoid, ok := val.(string) if !ok || ssoid == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } fmt.Println("✅ SSOID reçu depuis le contexte :", ssoid) // Récupérer l'utilisateur en base via le SSOID var user models.User if err := db.Where("sso_id = ?", ssoid).First(&user).Error; err != nil || user.ID == 0 { http.Error(w, "Utilisateur introuvable", http.StatusUnauthorized) return } var payload map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } fmt.Printf("📨 Payload reçu : %+v\n", payload) message, err := models.BuildMessageFromPayload(payload) if err != nil { http.Error(w, fmt.Sprintf("Error: %v", err), http.StatusBadRequest) return } jsonBody, err := json.MarshalIndent(message, "", " ") // joli format if err != nil { http.Error(w, "Failed to encode message", http.StatusInternalServerError) return } apiURL := fmt.Sprintf("https://graph.facebook.com/v22.0/%s/messages", user.WhatsappPhoneNumberID) fmt.Println("📡 Envoi de la requête à :", apiURL) fmt.Println("📦 JSON envoyé :") fmt.Println(string(jsonBody)) req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonBody)) if err != nil { http.Error(w, "Failed to create request", http.StatusInternalServerError) return } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+user.WhatsappToken) fmt.Println("📋 Headers :") for key, vals := range req.Header { for _, v := range vals { fmt.Printf(" %s: %s\n", key, v) } } resp, err := http.DefaultClient.Do(req) if err != nil { http.Error(w, "Failed to contact WhatsApp API", http.StatusBadGateway) return } defer resp.Body.Close() // 🛠 debug réponse respBody, _ := io.ReadAll(resp.Body) fmt.Printf("✅ Réponse WhatsApp (%d) : %s\n", resp.StatusCode, string(respBody)) w.WriteHeader(resp.StatusCode) w.Write(respBody) } } func AdminUserDelete(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] var user models.User if err := db.Unscoped().First(&user, id).Error; err != nil { http.Error(w, "Utilisateur introuvable", http.StatusNotFound) return } // Suppression définitive if err := db.Unscoped().Delete(&user).Error; err != nil { http.Error(w, "Erreur suppression", http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) w.Write([]byte("")) } }