Files
projet_gestion_commande/backend/gestion/db/redis_session.go
T
2026-03-27 22:23:46 +01:00

404 lines
12 KiB
Go

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
}