370 lines
11 KiB
Go
370 lines
11 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid" // Added import
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// PlaybackAnalyticsRateLimiter gère le rate limiting pour les analytics de playback
|
|
// T0389: Create Playback Analytics Rate Limiting
|
|
type PlaybackAnalyticsRateLimiter struct {
|
|
db *gorm.DB
|
|
logger *zap.Logger
|
|
|
|
// Rate limiting par utilisateur (requêtes par minute)
|
|
requestsPerMinute int
|
|
requestsWindow time.Duration
|
|
|
|
// Throttling (délai minimum entre requêtes)
|
|
minRequestInterval time.Duration
|
|
|
|
// Quotas (limites quotidiennes et hebdomadaires)
|
|
dailyQuota int
|
|
weeklyQuota int
|
|
|
|
// Cache en mémoire pour le rate limiting
|
|
mu sync.RWMutex
|
|
userRequests map[uuid.UUID][]time.Time // userID -> []time.Time
|
|
userLastRequest map[uuid.UUID]time.Time // userID -> last request time
|
|
userDailyCount map[uuid.UUID]int // userID -> daily count
|
|
userWeeklyCount map[uuid.UUID]int // userID -> weekly count
|
|
lastCleanup time.Time
|
|
}
|
|
|
|
// RateLimitConfig configuration pour le rate limiter
|
|
// T0389: Create Playback Analytics Rate Limiting
|
|
type RateLimitConfig struct {
|
|
RequestsPerMinute int // Nombre de requêtes par minute
|
|
RequestsWindow time.Duration // Fenêtre de temps pour les requêtes
|
|
MinRequestInterval time.Duration // Délai minimum entre requêtes (throttling)
|
|
DailyQuota int // Quota quotidien
|
|
WeeklyQuota int // Quota hebdomadaire
|
|
}
|
|
|
|
// DefaultRateLimitConfig retourne une configuration par défaut
|
|
func DefaultRateLimitConfig() RateLimitConfig {
|
|
return RateLimitConfig{
|
|
RequestsPerMinute: 60, // 60 requêtes par minute
|
|
RequestsWindow: 1 * time.Minute, // Fenêtre de 1 minute
|
|
MinRequestInterval: 1 * time.Second, // Minimum 1 seconde entre requêtes
|
|
DailyQuota: 10000, // 10000 analytics par jour
|
|
WeeklyQuota: 50000, // 50000 analytics par semaine
|
|
}
|
|
}
|
|
|
|
// NewPlaybackAnalyticsRateLimiter crée un nouveau rate limiter pour les analytics
|
|
// T0389: Create Playback Analytics Rate Limiting
|
|
func NewPlaybackAnalyticsRateLimiter(db *gorm.DB, logger *zap.Logger, config RateLimitConfig) *PlaybackAnalyticsRateLimiter {
|
|
if logger == nil {
|
|
logger = zap.NewNop()
|
|
}
|
|
|
|
limiter := &PlaybackAnalyticsRateLimiter{
|
|
db: db,
|
|
logger: logger,
|
|
requestsPerMinute: config.RequestsPerMinute,
|
|
requestsWindow: config.RequestsWindow,
|
|
minRequestInterval: config.MinRequestInterval,
|
|
dailyQuota: config.DailyQuota,
|
|
weeklyQuota: config.WeeklyQuota,
|
|
userRequests: make(map[uuid.UUID][]time.Time),
|
|
userLastRequest: make(map[uuid.UUID]time.Time),
|
|
userDailyCount: make(map[uuid.UUID]int),
|
|
userWeeklyCount: make(map[uuid.UUID]int),
|
|
lastCleanup: time.Now(),
|
|
}
|
|
|
|
// Démarrer le nettoyage périodique
|
|
go limiter.cleanup()
|
|
|
|
return limiter
|
|
}
|
|
|
|
// RateLimitResult représente le résultat d'une vérification de rate limit
|
|
// T0389: Create Playback Analytics Rate Limiting
|
|
type RateLimitResult struct {
|
|
Allowed bool
|
|
Reason string
|
|
RetryAfter time.Duration
|
|
Remaining int
|
|
QuotaUsed int
|
|
QuotaLimit int
|
|
}
|
|
|
|
// CheckRateLimit vérifie si une requête est autorisée selon les limites
|
|
// T0389: Create Playback Analytics Rate Limiting
|
|
func (rl *PlaybackAnalyticsRateLimiter) CheckRateLimit(ctx context.Context, userID uuid.UUID) (*RateLimitResult, error) {
|
|
rl.mu.Lock()
|
|
defer rl.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
|
|
// Nettoyer périodiquement le cache
|
|
if now.Sub(rl.lastCleanup) > 5*time.Minute {
|
|
rl.cleanupLocked(now)
|
|
rl.lastCleanup = now
|
|
}
|
|
|
|
// 1. Vérifier le throttling (délai minimum entre requêtes)
|
|
if lastRequest, exists := rl.userLastRequest[userID]; exists {
|
|
timeSinceLastRequest := now.Sub(lastRequest)
|
|
if timeSinceLastRequest < rl.minRequestInterval {
|
|
retryAfter := rl.minRequestInterval - timeSinceLastRequest
|
|
return &RateLimitResult{
|
|
Allowed: false,
|
|
Reason: "throttling: request too soon",
|
|
RetryAfter: retryAfter,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// 2. Vérifier le rate limiting (requêtes par minute)
|
|
cutoff := now.Add(-rl.requestsWindow)
|
|
validRequests := []time.Time{}
|
|
if requests, exists := rl.userRequests[userID]; exists {
|
|
for _, reqTime := range requests {
|
|
if reqTime.After(cutoff) {
|
|
validRequests = append(validRequests, reqTime)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(validRequests) >= rl.requestsPerMinute {
|
|
// Calculer le temps d'attente jusqu'à ce que la plus ancienne requête expire
|
|
oldestRequest := validRequests[0]
|
|
retryAfter := oldestRequest.Add(rl.requestsWindow).Sub(now)
|
|
if retryAfter < 0 {
|
|
retryAfter = 0
|
|
}
|
|
|
|
return &RateLimitResult{
|
|
Allowed: false,
|
|
Reason: fmt.Sprintf("rate limit exceeded: %d requests per %v", rl.requestsPerMinute, rl.requestsWindow),
|
|
RetryAfter: retryAfter,
|
|
Remaining: 0,
|
|
}, nil
|
|
}
|
|
|
|
// 3. Vérifier les quotas (quotas quotidiens et hebdomadaires)
|
|
dailyCount, weeklyCount, err := rl.getQuotaCounts(ctx, userID, now)
|
|
if err != nil {
|
|
rl.logger.Warn("Failed to get quota counts, using cache",
|
|
zap.Error(err),
|
|
zap.String("user_id", userID.String())) // Utiliser les valeurs en cache en cas d'erreur
|
|
dailyCount = rl.userDailyCount[userID]
|
|
weeklyCount = rl.userWeeklyCount[userID]
|
|
}
|
|
|
|
if dailyCount >= rl.dailyQuota {
|
|
return &RateLimitResult{
|
|
Allowed: false,
|
|
Reason: fmt.Sprintf("daily quota exceeded: %d/%d", dailyCount, rl.dailyQuota),
|
|
RetryAfter: timeUntilMidnight(now),
|
|
QuotaUsed: dailyCount,
|
|
QuotaLimit: rl.dailyQuota,
|
|
}, nil
|
|
}
|
|
|
|
if weeklyCount >= rl.weeklyQuota {
|
|
return &RateLimitResult{
|
|
Allowed: false,
|
|
Reason: fmt.Sprintf("weekly quota exceeded: %d/%d", weeklyCount, rl.weeklyQuota),
|
|
RetryAfter: timeUntilNextWeek(now),
|
|
QuotaUsed: weeklyCount,
|
|
QuotaLimit: rl.weeklyQuota,
|
|
}, nil
|
|
}
|
|
|
|
// Toutes les vérifications passées, autoriser la requête
|
|
validRequests = append(validRequests, now)
|
|
rl.userRequests[userID] = validRequests
|
|
rl.userLastRequest[userID] = now
|
|
|
|
remaining := rl.requestsPerMinute - len(validRequests)
|
|
|
|
return &RateLimitResult{
|
|
Allowed: true,
|
|
Remaining: remaining,
|
|
QuotaUsed: dailyCount,
|
|
QuotaLimit: rl.dailyQuota,
|
|
}, nil
|
|
}
|
|
|
|
// RecordRequest enregistre une requête (appelé après qu'une requête a été traitée avec succès)
|
|
// T0389: Create Playback Analytics Rate Limiting
|
|
func (rl *PlaybackAnalyticsRateLimiter) RecordRequest(ctx context.Context, userID uuid.UUID) error {
|
|
rl.mu.Lock()
|
|
defer rl.mu.Unlock()
|
|
|
|
// Mettre à jour les compteurs de quota
|
|
rl.userDailyCount[userID]++
|
|
rl.userWeeklyCount[userID]++
|
|
|
|
// Enregistrer dans la base de données pour persistance
|
|
// Note: On pourrait créer une table de quotas si nécessaire
|
|
// Pour l'instant, on utilise uniquement le cache en mémoire
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetQuotaInfo retourne les informations de quota pour un utilisateur
|
|
// T0389: Create Playback Analytics Rate Limiting
|
|
func (rl *PlaybackAnalyticsRateLimiter) GetQuotaInfo(ctx context.Context, userID uuid.UUID) (map[string]interface{}, error) {
|
|
rl.mu.RLock()
|
|
defer rl.mu.RUnlock()
|
|
|
|
now := time.Now()
|
|
dailyCount, weeklyCount, err := rl.getQuotaCounts(ctx, userID, now)
|
|
if err != nil {
|
|
// Utiliser les valeurs en cache
|
|
dailyCount = rl.userDailyCount[userID]
|
|
weeklyCount = rl.userWeeklyCount[userID]
|
|
}
|
|
|
|
// Calculer les requêtes restantes dans la fenêtre actuelle
|
|
cutoff := now.Add(-rl.requestsWindow)
|
|
validRequests := []time.Time{}
|
|
if requests, exists := rl.userRequests[userID]; exists {
|
|
for _, reqTime := range requests {
|
|
if reqTime.After(cutoff) {
|
|
validRequests = append(validRequests, reqTime)
|
|
}
|
|
}
|
|
}
|
|
remainingRequests := rl.requestsPerMinute - len(validRequests)
|
|
if remainingRequests < 0 {
|
|
remainingRequests = 0
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"rate_limit": map[string]interface{}{
|
|
"requests_per_minute": rl.requestsPerMinute,
|
|
"remaining": remainingRequests,
|
|
"window": rl.requestsWindow.String(),
|
|
},
|
|
"throttling": map[string]interface{}{
|
|
"min_interval": rl.minRequestInterval.String(),
|
|
},
|
|
"quotas": map[string]interface{}{
|
|
"daily": map[string]interface{}{
|
|
"used": dailyCount,
|
|
"limit": rl.dailyQuota,
|
|
"remaining": rl.dailyQuota - dailyCount,
|
|
},
|
|
"weekly": map[string]interface{}{
|
|
"used": weeklyCount,
|
|
"limit": rl.weeklyQuota,
|
|
"remaining": rl.weeklyQuota - weeklyCount,
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// getQuotaCounts récupère les compteurs de quota depuis la base de données
|
|
// T0389: Create Playback Analytics Rate Limiting
|
|
func (rl *PlaybackAnalyticsRateLimiter) getQuotaCounts(ctx context.Context, userID uuid.UUID, now time.Time) (int, int, error) {
|
|
// Calculer les dates de début
|
|
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
startOfWeek := startOfDay
|
|
weekday := int(now.Weekday())
|
|
if weekday == 0 {
|
|
weekday = 7 // Dimanche = 7
|
|
}
|
|
startOfWeek = startOfWeek.AddDate(0, 0, -weekday+1) // Lundi
|
|
|
|
// Compter les analytics enregistrées aujourd'hui
|
|
var dailyCount int64
|
|
err := rl.db.WithContext(ctx).
|
|
Model(&struct {
|
|
Count int64
|
|
}{}).
|
|
Select("COUNT(*)").
|
|
Table("playback_analytics").
|
|
Where("user_id = ? AND created_at >= ?", userID.String(), startOfDay).
|
|
Scan(&dailyCount).Error
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
// Compter les analytics enregistrées cette semaine
|
|
var weeklyCount int64
|
|
err = rl.db.WithContext(ctx).
|
|
Model(&struct {
|
|
Count int64
|
|
}{}).
|
|
Select("COUNT(*)").
|
|
Table("playback_analytics").
|
|
Where("user_id = ? AND created_at >= ?", userID.String(), startOfWeek).
|
|
Scan(&weeklyCount).Error
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
return int(dailyCount), int(weeklyCount), nil
|
|
}
|
|
|
|
// cleanup nettoie périodiquement le cache
|
|
// T0389: Create Playback Analytics Rate Limiting
|
|
func (rl *PlaybackAnalyticsRateLimiter) cleanup() {
|
|
ticker := time.NewTicker(5 * time.Minute)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
rl.mu.Lock()
|
|
rl.cleanupLocked(time.Now())
|
|
rl.lastCleanup = time.Now()
|
|
rl.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// cleanupLocked nettoie le cache (doit être appelé avec le mutex verrouillé)
|
|
// T0389: Create Playback Analytics Rate Limiting
|
|
func (rl *PlaybackAnalyticsRateLimiter) cleanupLocked(now time.Time) {
|
|
cutoff := now.Add(-rl.requestsWindow)
|
|
|
|
// Nettoyer les requêtes expirées
|
|
for userID, requests := range rl.userRequests {
|
|
validRequests := []time.Time{}
|
|
for _, reqTime := range requests {
|
|
if reqTime.After(cutoff) {
|
|
validRequests = append(validRequests, reqTime)
|
|
}
|
|
}
|
|
if len(validRequests) == 0 {
|
|
delete(rl.userRequests, userID)
|
|
} else {
|
|
rl.userRequests[userID] = validRequests
|
|
}
|
|
}
|
|
|
|
// Nettoyer les dernières requêtes si trop anciennes
|
|
cutoffLastRequest := now.Add(-1 * time.Hour)
|
|
for userID, lastRequest := range rl.userLastRequest {
|
|
if lastRequest.Before(cutoffLastRequest) {
|
|
delete(rl.userLastRequest, userID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// timeUntilMidnight calcule le temps jusqu'à minuit
|
|
func timeUntilMidnight(now time.Time) time.Duration {
|
|
midnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location())
|
|
return midnight.Sub(now)
|
|
}
|
|
|
|
// timeUntilNextWeek calcule le temps jusqu'au prochain lundi
|
|
func timeUntilNextWeek(now time.Time) time.Duration {
|
|
weekday := int(now.Weekday())
|
|
if weekday == 0 {
|
|
weekday = 7 // Dimanche = 7
|
|
}
|
|
daysUntilMonday := 8 - weekday // Jours jusqu'au prochain lundi
|
|
nextMonday := time.Date(now.Year(), now.Month(), now.Day()+daysUntilMonday, 0, 0, 0, 0, now.Location())
|
|
return nextMonday.Sub(now)
|
|
}
|