whatapp-go-pvnet/backend/handlers/main.go

500 lines
14 KiB
Go
Raw Normal View History

2025-05-09 08:14:22 +00:00
package handlers
import (
"bytes"
"cangui/whatsapp/backend/jwt"
"cangui/whatsapp/backend/models"
"encoding/json"
"fmt"
"io"
"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 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)
}
}
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: "inbound",
})
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["text"].(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")
2025-05-09 08:39:12 +00:00
if mode == "subscribe" && token == "secrettoken" {
2025-05-09 08:14:22 +00:00
w.WriteHeader(http.StatusOK)
w.Write([]byte(challenge))
return
}
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Invalid verify token"))
}
}
2025-05-09 08:52:26 +00:00
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("📩 Webhook reçu :", body)
// Exemple de parsing
entry := body["entry"].([]interface{})[0].(map[string]interface{})
change := entry["changes"].([]interface{})[0].(map[string]interface{})
value := change["value"].(map[string]interface{})
messages := value["messages"].([]interface{})
if len(messages) > 0 {
msg := messages[0].(map[string]interface{})
from := msg["from"].(string)
msgType := msg["type"].(string)
var content string
if msgType == "text" {
content = msg["text"].(map[string]interface{})["body"].(string)
}
fmt.Printf("💬 Message reçu de %s : %s\n", from, content)
// TODO : enregistrer en base
}
w.WriteHeader(http.StatusOK)
}
}
2025-05-09 08:14:22 +00:00
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)
}
2025-05-09 09:23:54 +00:00
}
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()
2025-05-09 13:21:44 +00:00
// Lire la réponse pour capturer le message ID ou statut
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"])
}
}
err = models.SaveMessageStatusError(db, user.ID, to, messageID, "sent", fmt.Sprintf("Template: %s", templateName))
if err != nil {
fmt.Println("⚠️ Erreur enregistrement statut message:", err)
}
w.WriteHeader(resp.StatusCode)
json.NewEncoder(w).Encode(respBody)
2025-05-09 09:23:54 +00:00
}
}
2025-05-09 13:21:44 +00:00