130 lines
3.8 KiB
Go
130 lines
3.8 KiB
Go
package services
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
"veza-backend-api/internal/models"
|
|
)
|
|
|
|
// 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)
|
|
expiresAt := time.Now().Add(ttl)
|
|
|
|
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)
|
|
refreshToken := &models.RefreshToken{
|
|
UserID: userID,
|
|
TokenHash: newTokenHash,
|
|
ExpiresAt: time.Now().Add(ttl),
|
|
}
|
|
|
|
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)
|
|
}
|