package db import ( "encoding/json" "fmt" "gestion/models" "log" "time" ) // ============================================ // SESSION REDIS - GESTION UTILISATEUR // ============================================ // SessionData représente une session client en Redis type SessionData struct { ClientID int `json:"client_id"` Username string `json:"username"` SessionID string `json:"session_id"` Role string `json:"role"` CreatedAt int64 `json:"created_at"` LastActivity int64 `json:"last_activity"` ExpiresAt int64 `json:"expires_at"` BasketVersion int `json:"basket_version"` PointsCache int `json:"points_cache"` PenaltyCache float64 `json:"penalty_cache"` Tags []string `json:"tags,omitempty"` } // ============================================ // CRÉER UNE SESSION CLIENT // ============================================ // CreateClientSession crée une session Redis pour un client authentifié // Appelé depuis handlers/auth.go après LoginClient réussi // // Exemple d'utilisation: // // sessionID := uuid.New().String() // database.CreateClientSession(clientID, username, sessionID) func (d *Database) CreateClientSession(clientID int, username string, sessionID string) error { log.Printf("📝 [SESSION] Création session pour client: %s (ID: %d)", username, clientID) sessionKey := fmt.Sprintf("session:client:%d", clientID) now := time.Now().Unix() expiresAt := now + (5 * 3600) // 5 heures sessionData := SessionData{ ClientID: clientID, Username: username, SessionID: sessionID, Role: "client", CreatedAt: now, LastActivity: now, ExpiresAt: expiresAt, BasketVersion: 0, PointsCache: 0, PenaltyCache: 0, } // Sérialiser et sauvegarder sessionJSON, err := json.Marshal(sessionData) if err != nil { log.Printf("❌ [SESSION] Erreur sérialisation: %v", err) return fmt.Errorf("erreur sérialisation session: %w", err) } ttl := time.Duration(expiresAt-now) * time.Second if err := Redis.Set(RedisCtx, sessionKey, sessionJSON, ttl).Err(); err != nil { log.Printf("❌ [SESSION] Erreur sauvegarde Redis: %v", err) return fmt.Errorf("erreur sauvegarde session Redis: %w", err) } // Ajouter à l'index des sessions actives if err := Redis.SAdd(RedisCtx, "session:active:clients", clientID).Err(); err != nil { log.Printf("⚠️ [SESSION] Erreur ajout index: %v", err) } // Charger les infos du client (points, penalty) dans le cache if client, err := d.GetClientByUsername(username); err == nil { sessionData.PenaltyCache = float64(client.Amende) sessionJSON, _ := json.Marshal(sessionData) Redis.Set(RedisCtx, sessionKey, sessionJSON, ttl) } log.Printf("✅ [SESSION] Session créée pour %s - TTL: 5h", username) return nil } // ============================================ // RÉCUPÉRER UNE SESSION CLIENT // ============================================ // GetClientSession récupère la session Redis d'un client // Retourne nil si session expirée ou inexistante func (d *Database) GetClientSession(clientID int) (*SessionData, error) { sessionKey := fmt.Sprintf("session:client:%d", clientID) data, err := Redis.Get(RedisCtx, sessionKey).Result() if err != nil { log.Printf("⚠️ [SESSION] Pas de session trouvée pour client %d", clientID) return nil, fmt.Errorf("session non trouvée") } var session SessionData if err := json.Unmarshal([]byte(data), &session); err != nil { log.Printf("❌ [SESSION] Erreur désérialisation: %v", err) return nil, fmt.Errorf("erreur désérialisation: %w", err) } // Vérifier si session expirée if time.Now().Unix() > session.ExpiresAt { log.Printf("⚠️ [SESSION] Session expirée pour client %d", clientID) Redis.Del(RedisCtx, sessionKey) return nil, fmt.Errorf("session expirée") } return &session, nil } // ============================================ // METTRE À JOUR L'ACTIVITÉ DE SESSION // ============================================ // RefreshSessionTimeout prolonge la durée de vie de la session // Appelé régulièrement par SessionMiddleware (chaque requête client) func (d *Database) RefreshSessionTimeout(clientID int) error { sessionKey := fmt.Sprintf("session:client:%d", clientID) // Récupérer la session data, err := Redis.Get(RedisCtx, sessionKey).Result() if err != nil { return fmt.Errorf("session non trouvée") } var session SessionData if err := json.Unmarshal([]byte(data), &session); err != nil { return fmt.Errorf("erreur désérialisation: %w", err) } // Mettre à jour lastActivity et expiresAt now := time.Now().Unix() session.LastActivity = now session.ExpiresAt = now + (5 * 3600) // Prolonger de 5 heures // Resauvegarder sessionJSON, _ := json.Marshal(session) ttl := time.Duration(session.ExpiresAt-now) * time.Second if err := Redis.Set(RedisCtx, sessionKey, sessionJSON, ttl).Err(); err != nil { log.Printf("⚠️ [SESSION] Erreur refresh: %v", err) return nil // Pas critique } return nil } // ============================================ // INVALIDER UNE SESSION (LOGOUT) // ============================================ // InvalidateSession supprime la session Redis (logout) // Appelé depuis handlers/auth.go dans LogoutClient func (d *Database) InvalidateSession(clientID int) error { sessionKey := fmt.Sprintf("session:client:%d", clientID) basketKey := fmt.Sprintf("session:basket:%d", clientID) // Supprimer la session if err := Redis.Del(RedisCtx, sessionKey).Err(); err != nil { log.Printf("⚠️ [SESSION] Erreur suppression: %v", err) } // Vider le panier Redis if err := Redis.Del(RedisCtx, basketKey).Err(); err != nil { log.Printf("⚠️ [SESSION] Erreur suppression panier: %v", err) } // Retirer de l'index if err := Redis.SRem(RedisCtx, "session:active:clients", clientID).Err(); err != nil { log.Printf("⚠️ [SESSION] Erreur retrait index: %v", err) } log.Printf("✅ [SESSION] Session invalidée pour client %d", clientID) return nil } // ============================================ // PANIER EN CACHE REDIS // ============================================ // BasketItemCache représente un item du panier en cache type BasketItemCache struct { ID int `json:"id"` ProductID int `json:"product_id"` ProductName string `json:"product_name"` Quantity int `json:"quantity"` Price float64 `json:"price"` Category string `json:"category"` AddedAt int64 `json:"added_at"` } // GetSessionBasket récupère le panier en cache Redis // Retourne les items du panier avec total func (d *Database) GetSessionBasket(clientID int) ([]BasketItemCache, float64, error) { basketKey := fmt.Sprintf("session:basket:%d", clientID) // Récupérer tous les items du panier items, err := Redis.HGetAll(RedisCtx, basketKey).Result() if err != nil { log.Printf("⚠️ [BASKET] Pas de panier en cache pour client %d", clientID) return []BasketItemCache{}, 0, nil } var basketItems []BasketItemCache var totalPrice float64 for _, itemJSON := range items { var item BasketItemCache if err := json.Unmarshal([]byte(itemJSON), &item); err != nil { log.Printf("⚠️ [BASKET] Erreur parsing item: %v", err) continue } basketItems = append(basketItems, item) totalPrice += item.Price * float64(item.Quantity) } return basketItems, totalPrice, nil } // UpdateSessionBasket met à jour le panier en cache Redis // Appelé après ajout/modification d'un produit au panier func (d *Database) UpdateSessionBasket(clientID int, basketItems []BasketItemCache) error { basketKey := fmt.Sprintf("session:basket:%d", clientID) // Vider le panier existant Redis.Del(RedisCtx, basketKey) // Ajouter tous les items for _, item := range basketItems { itemJSON, _ := json.Marshal(item) if err := Redis.HSet(RedisCtx, basketKey, item.ProductID, itemJSON).Err(); err != nil { log.Printf("⚠️ [BASKET] Erreur ajout item: %v", err) } } // TTL: 24 heures if err := Redis.Expire(RedisCtx, basketKey, 24*time.Hour).Err(); err != nil { log.Printf("⚠️ [BASKET] Erreur TTL: %v", err) } return nil } // ClearSessionBasket vide le panier en cache Redis // Appelé après validation de commande (checkout) func (d *Database) ClearSessionBasket(clientID int) error { basketKey := fmt.Sprintf("session:basket:%d", clientID) if err := Redis.Del(RedisCtx, basketKey).Err(); err != nil { log.Printf("⚠️ [BASKET] Erreur clear: %v", err) return nil } log.Printf("✅ [BASKET] Panier vidé pour client %d", clientID) return nil } // ============================================ // UTILITAIRES SESSION // ============================================ // GetAllActiveSessions récupère toutes les sessions actives // Utile pour admin/stats func (d *Database) GetAllActiveSessions() ([]SessionData, error) { clientIDs, err := Redis.SMembers(RedisCtx, "session:active:clients").Result() if err != nil { return nil, fmt.Errorf("erreur récupération sessions: %w", err) } var sessions []SessionData for _, clientIDStr := range clientIDs { var clientID int if _, err := fmt.Sscanf(clientIDStr, "%d", &clientID); err != nil { continue } if session, err := d.GetClientSession(clientID); err == nil { sessions = append(sessions, *session) } } return sessions, nil } // GetSessionCount retourne le nombre de sessions actives func (d *Database) GetSessionCount() (int64, error) { count, err := Redis.SCard(RedisCtx, "session:active:clients").Result() if err != nil { return 0, fmt.Errorf("erreur comptage sessions: %w", err) } return count, nil } // ============================================ // CACHE PROFIL CLIENT // ============================================ // CacheClientProfile met en cache les infos du client (pour 1h) func (d *Database) CacheClientProfile(client interface{}) error { // Récupérer le client depuis DB si c'est un username var clientData *models.Client // Si c'est un username string if username, ok := client.(string); ok { var err error clientData, err = d.GetClientByUsername(username) if err != nil { return fmt.Errorf("client non trouvé: %w", err) } } else { // Si c'est déjà un *models.Client clientData = client.(*models.Client) } cacheKey := fmt.Sprintf("cache:client:profile:%d", clientData.ID) // Sérialiser profileJSON, err := json.Marshal(clientData) if err != nil { return fmt.Errorf("erreur sérialisation: %w", err) } // Sauvegarder avec TTL 1h if err := Redis.Set(RedisCtx, cacheKey, profileJSON, 1*time.Hour).Err(); err != nil { return fmt.Errorf("erreur cache: %w", err) } log.Printf("✅ [CACHE] Profil client %d mis en cache (1h)", clientData.ID) return nil } // GetCachedClientProfile récupère le profil en cache func (d *Database) GetCachedClientProfile(clientID int) (*models.Client, error) { cacheKey := fmt.Sprintf("cache:client:profile:%d", clientID) data, err := Redis.Get(RedisCtx, cacheKey).Result() if err != nil { return nil, fmt.Errorf("cache miss") } var client models.Client if err := json.Unmarshal([]byte(data), &client); err != nil { return nil, fmt.Errorf("erreur désérialisation: %w", err) } return &client, nil } // InvalidateClientCache invalide le cache du client func (d *Database) InvalidateClientCache(clientID int) error { cacheKey := fmt.Sprintf("cache:client:profile:%d", clientID) if err := Redis.Del(RedisCtx, cacheKey).Err(); err != nil { return fmt.Errorf("erreur invalidation: %w", err) } log.Printf("✅ [CACHE] Profil client %d invalidé", clientID) return nil } // ============================================ // COMMANDES EN CACHE (POUR TRACKING) // ============================================ // CacheCommandInfo met en cache les infos d'une commande func (d *Database) CacheCommandInfo(commandID int, command map[string]interface{}) error { cacheKey := fmt.Sprintf("cache:command:%d", commandID) commandJSON, err := json.Marshal(command) if err != nil { return fmt.Errorf("erreur sérialisation: %w", err) } // TTL: 4 heures if err := Redis.Set(RedisCtx, cacheKey, commandJSON, 4*time.Hour).Err(); err != nil { return fmt.Errorf("erreur cache: %w", err) } return nil } // GetCachedCommand récupère une commande en cache func (d *Database) GetCachedCommand(commandID int) (map[string]interface{}, error) { cacheKey := fmt.Sprintf("cache:command:%d", commandID) data, err := Redis.Get(RedisCtx, cacheKey).Result() if err != nil { return nil, fmt.Errorf("cache miss") } var command map[string]interface{} if err := json.Unmarshal([]byte(data), &command); err != nil { return nil, fmt.Errorf("erreur désérialisation: %w", err) } return command, nil }