package services import ( "crypto/sha256" "encoding/hex" "errors" "time" "veza-backend-api/internal/models" "github.com/google/uuid" "gorm.io/gorm" ) // RefreshTokenService gère le stockage et la validation des refresh tokens // T0164: Service pour gérer les refresh tokens avec stockage en base et validation type RefreshTokenService struct { db *gorm.DB } // NewRefreshTokenService crée une nouvelle instance de RefreshTokenService func NewRefreshTokenService(db *gorm.DB) *RefreshTokenService { return &RefreshTokenService{db: db} } // Store stocke un refresh token en base de données (hashé pour la sécurité) // T0164: Stocke le token hashé avec userID et expiration // MIGRATION UUID: userID migré vers uuid.UUID func (s *RefreshTokenService) Store(userID uuid.UUID, token string, ttl time.Duration) error { tokenHash := s.hashToken(token) // Ajouter un buffer de 1 seconde pour garantir que expires_at > created_at // Cela évite les problèmes de précision des timestamps entre Go et PostgreSQL expiresAt := time.Now().Add(ttl).Add(1 * time.Second) refreshToken := &models.RefreshToken{ UserID: userID, TokenHash: tokenHash, ExpiresAt: expiresAt, } return s.db.Create(refreshToken).Error } // Validate vérifie si un refresh token est valide // T0164: Valide le token en vérifiant son hash et sa date d'expiration // MIGRATION UUID: userID migré vers uuid.UUID func (s *RefreshTokenService) Validate(userID uuid.UUID, token string) error { tokenHash := s.hashToken(token) var refreshToken models.RefreshToken err := s.db.Where("user_id = ? AND token_hash = ?", userID, tokenHash). First(&refreshToken).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("refresh token not found") } return err } // Vérifier si le token n'a pas expiré if time.Now().After(refreshToken.ExpiresAt) { return errors.New("refresh token expired") } return nil } // Rotate invalide l'ancien refresh token et en stocke un nouveau // MIGRATION UUID: userID migré vers uuid.UUID func (s *RefreshTokenService) Rotate(userID uuid.UUID, oldToken, newToken string, ttl time.Duration) error { // Transaction pour assurer l'atomicité return s.db.Transaction(func(tx *gorm.DB) error { // Révoquer l'ancien oldTokenHash := s.hashToken(oldToken) if err := tx.Where("user_id = ? AND token_hash = ?", userID, oldTokenHash).Delete(&models.RefreshToken{}).Error; err != nil { return err } // Stocker le nouveau newTokenHash := s.hashToken(newToken) // Ajouter un buffer de 1 seconde pour garantir que expires_at > created_at // Cela évite les problèmes de précision des timestamps entre Go et PostgreSQL refreshToken := &models.RefreshToken{ UserID: userID, TokenHash: newTokenHash, ExpiresAt: time.Now().Add(ttl).Add(1 * time.Second), } return tx.Create(refreshToken).Error }) } // Revoke supprime/révoque un refresh token // T0164: Supprime le token de la base de données // MIGRATION UUID: userID migré vers uuid.UUID func (s *RefreshTokenService) Revoke(userID uuid.UUID, token string) error { tokenHash := s.hashToken(token) result := s.db.Where("user_id = ? AND token_hash = ?", userID, tokenHash). Delete(&models.RefreshToken{}) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { // Ce n'est pas forcément une erreur critique si le token n'existait déjà plus return nil } return nil } // RevokeAll révoque tous les refresh tokens d'un utilisateur // Utile pour la déconnexion de tous les appareils // MIGRATION UUID: userID migré vers uuid.UUID func (s *RefreshTokenService) RevokeAll(userID uuid.UUID) error { result := s.db.Where("user_id = ?", userID). Delete(&models.RefreshToken{}) return result.Error } // hashToken hash un token avec SHA-256 pour le stockage sécurisé func (s *RefreshTokenService) hashToken(token string) string { hash := sha256.Sum256([]byte(token)) return hex.EncodeToString(hash[:]) } // HashToken expose la méthode hashToken pour les tests // T0171: Méthode publique pour hasher les tokens (utilisée dans les tests) func (s *RefreshTokenService) HashToken(token string) string { return s.hashToken(token) }