diff --git a/backend/main.go b/backend/main.go index 1fb4b4e..d7e8da9 100644 --- a/backend/main.go +++ b/backend/main.go @@ -20,8 +20,6 @@ func main() { // 3. Routes non protégées : on les monte sur le routeur principal routes.RoutesPublic(r, bd) - - // 4. Créer un sous-routeur pour les routes protégées protected := r.PathPrefix("/").Subrouter() @@ -29,7 +27,9 @@ func main() { protected.Use(middleware.AuthMiddleware) // 6. Enregistrer les routes protégées sur ce sous-routeur routes.RoutesProtected(protected, bd) - + r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/login", http.StatusSeeOther) + }) // 7. Lancer le serveur sur le port 4000 log.Fatal(http.ListenAndServe(":3003", r)) } \ No newline at end of file diff --git a/backend/middleware/middleware.go b/backend/middleware/middleware.go index 872dfcd..c206399 100644 --- a/backend/middleware/middleware.go +++ b/backend/middleware/middleware.go @@ -15,86 +15,71 @@ import ( var secretKey = []byte("secret-key") +// AuthMiddleware protège les routes via JWT et redirige vers /login si non authentifié func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var tokenString string - - // 1. Vérifie Authorization: Bearer ... - if authHeader := r.Header.Get("Authorization"); strings.HasPrefix(authHeader, "Bearer ") { - tokenString = strings.TrimPrefix(authHeader, "Bearer ") - } - - // 2. Vérifie le cookie si pas de header - if tokenString == "" { - if cookie, err := r.Cookie("token"); err == nil { - tokenString = cookie.Value - } - } - - // 3. Vérifie le body JSON uniquement si POST ou PUT - if tokenString == "" && r.Method == http.MethodPost || r.Method == http.MethodPut { - if strings.Contains(r.Header.Get("Content-Type"), "application/json") { - bodyCopy, _ := io.ReadAll(r.Body) - var bodyMap map[string]interface{} - if err := json.Unmarshal(bodyCopy, &bodyMap); err == nil { - if val, ok := bodyMap["token"].(string); ok { - tokenString = val - // restaure le body - r.Body = io.NopCloser(bytes.NewReader(bodyCopy)) - } - } - } - } - - if tokenString == "" { - http.Error(w, "Token manquant", http.StatusUnauthorized) - return - } - - // ✅ Vérifie et parse le token - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("méthode signature invalide") - } - return secretKey, nil - }) - - if err != nil || !token.Valid { - http.Error(w, "Token invalide", http.StatusUnauthorized) - return - } - - claims, ok := token.Claims.(jwt.MapClaims) + ctx, ok := RedirectToLoginIfUnauthenticated(w, r) if !ok { - http.Error(w, "Claims invalides", http.StatusUnauthorized) return } - - // vérifie l’expiration - if exp, ok := claims["exp"].(float64); ok { - if time.Now().Unix() > int64(exp) { - http.Error(w, "Token expiré", http.StatusUnauthorized) - return - } - } - - // récupère l’identifiant - ssoid, ok := claims["username"].(string) - if !ok { - http.Error(w, "SSOID manquant", http.StatusUnauthorized) - return - } - - // injection dans le contexte - ctx := context.WithValue(r.Context(), "ssoid", ssoid) next.ServeHTTP(w, r.WithContext(ctx)) }) } +// RedirectToLoginIfUnauthenticated vérifie le JWT et injecte ssoid dans le contexte, sinon redirige vers /login +func RedirectToLoginIfUnauthenticated(w http.ResponseWriter, r *http.Request) (context.Context, bool) { + var tokenString string -// helper pour remarshal le JSON quand il est relu après parse -func mustMarshal(data map[string]interface{}) []byte { - b, _ := json.Marshal(data) - return b + if authHeader := r.Header.Get("Authorization"); strings.HasPrefix(authHeader, "Bearer ") { + tokenString = strings.TrimPrefix(authHeader, "Bearer ") + } + if tokenString == "" { + if cookie, err := r.Cookie("token"); err == nil { + tokenString = cookie.Value + } + } + if tokenString == "" && r.Method == http.MethodPost || r.Method == http.MethodPut { + if strings.Contains(r.Header.Get("Content-Type"), "application/json") { + bodyCopy, _ := io.ReadAll(r.Body) + var bodyMap map[string]interface{} + if err := json.Unmarshal(bodyCopy, &bodyMap); err == nil { + if val, ok := bodyMap["token"].(string); ok { + tokenString = val + r.Body = io.NopCloser(bytes.NewReader(bodyCopy)) + } + } + } + } + if tokenString == "" { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return nil, false + } + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("méthode signature invalide") + } + return secretKey, nil + }) + if err != nil || !token.Valid { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return nil, false + } + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return nil, false + } + if exp, ok := claims["exp"].(float64); ok { + if time.Now().Unix() > int64(exp) { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return nil, false + } + } + ssoid, ok := claims["username"].(string) + if !ok || ssoid == "" { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return nil, false + } + ctx := context.WithValue(r.Context(), "ssoid", ssoid) + return ctx, true } - diff --git a/backend/routes/routes.go b/backend/routes/routes.go index 1cf7327..76a5703 100644 --- a/backend/routes/routes.go +++ b/backend/routes/routes.go @@ -5,6 +5,7 @@ import ( "cangui/whatsapp/backend/renders" "net/http" + "github.com/golang-jwt/jwt" "github.com/gorilla/mux" "gorm.io/gorm" ) @@ -17,7 +18,27 @@ func RoutesPublic(r *mux.Router, db *gorm.DB) { r.PathPrefix("/frontend/assets/").Handler( http.StripPrefix("/frontend/assets/", http.FileServer(http.Dir(staticDir))), ) + r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Tente de lire le cookie + cookie, err := r.Cookie("token") + if err != nil || cookie.Value == "" { + // Redirige vers login si pas de cookie + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + // Si cookie présent, tente de parser + token, err := jwt.Parse(cookie.Value, func(token *jwt.Token) (interface{}, error) { + return []byte("secret-key"), nil + }) + if err != nil || !token.Valid { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + + // Sinon on va vers le dashboard + http.Redirect(w, r, "/dashboard", http.StatusSeeOther) + }) // Page de login r.HandleFunc("/login", renders.Login) r.HandleFunc("/api/whatsapp/webhook", handlers.WebhookVerifyHandler()).Methods("GET") diff --git a/frontend/templates/apidoc.pages.tmpl b/frontend/templates/apidoc.pages.tmpl index 37ee0bb..5e7ace0 100644 --- a/frontend/templates/apidoc.pages.tmpl +++ b/frontend/templates/apidoc.pages.tmpl @@ -36,13 +36,16 @@

📤 Envoi message template

POST /api/message/send2

{
-  "to": "33612345678",
-  "template_name": "hello_world",
-  "language": "fr",
-  "param1": "Jean",
-  "param2": "commande #1234",
-  "token": "votre_jwt_token"
-}
+ "to": "33612345678", + "template_name": "hello_world", + "language": "fr", + "param1": "Jean", + "param2": "commande #1234", + "param3": "optionnel", + "param4": "optionnel", + "param5": "optionnel", + "token": "votre_jwt_token" + }