package services import ( "context" "crypto/rand" "database/sql" "encoding/base64" "fmt" "time" "github.com/google/uuid" // Added import "veza-backend-api/internal/database" "veza-backend-api/internal/utils" "go.uber.org/zap" ) // PasswordResetService gère la génération, le stockage et la validation des tokens de réinitialisation de mot de passe // T0192: Service pour gérer les tokens de réinitialisation de mot de passe avec expiration et invalidation type PasswordResetService struct { db *database.Database logger *zap.Logger } // NewPasswordResetService crée une nouvelle instance de PasswordResetService func NewPasswordResetService(db *database.Database, logger *zap.Logger) *PasswordResetService { return &PasswordResetService{ db: db, logger: logger, } } // GenerateToken génère un token aléatoire sécurisé de 32 bytes encodé en base64 URL-safe // T0192: Génère un token aléatoire pour la réinitialisation de mot de passe func (s *PasswordResetService) GenerateToken() (string, error) { bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { s.logger.Error("Failed to generate random token", zap.Error(err)) return "", fmt.Errorf("failed to generate token: %w", err) } return base64.URLEncoding.EncodeToString(bytes), nil } // StoreToken stocke un token de réinitialisation en base de données avec une expiration de 1h // T0192: Sauvegarde le token avec expiration 1h func (s *PasswordResetService) StoreToken(userID uuid.UUID, token string) error { ctx := context.Background() expiresAt := time.Now().Add(1 * time.Hour) _, err := s.db.ExecContext(ctx, "INSERT INTO password_reset_tokens (user_id, token, expires_at, used) VALUES ($1, $2, $3, FALSE)", userID, token, expiresAt, ) if err != nil { s.logger.Error("Failed to store password reset token", zap.String("user_id", userID.String()), zap.Error(err), ) return fmt.Errorf("failed to store token: %w", err) } s.logger.Info("Password reset token stored", zap.String("user_id", userID.String()), zap.Time("expires_at", expiresAt), ) return nil } // VerifyToken valide un token de réinitialisation, vérifie son expiration et s'il n'a pas déjà été utilisé // T0192: Valide le token, vérifie l'expiration et s'il n'est pas déjà utilisé func (s *PasswordResetService) VerifyToken(token string) (uuid.UUID, error) { ctx := context.Background() var userID uuid.UUID var expiresAt time.Time var used bool err := s.db.QueryRowContext(ctx, "SELECT user_id, expires_at, used FROM password_reset_tokens WHERE token = $1", token, ).Scan(&userID, &expiresAt, &used) if err == sql.ErrNoRows { tokenPreview := token if len(token) > 8 { tokenPreview = token[:8] + "..." } s.logger.Warn("Password reset token not found", zap.String("token", tokenPreview)) return uuid.Nil, fmt.Errorf("invalid token") } if err != nil { s.logger.Error("Failed to verify token", zap.Error(err)) return uuid.Nil, fmt.Errorf("failed to verify token: %w", err) } if used { tokenPreview := token if len(token) > 8 { tokenPreview = token[:8] + "..." } s.logger.Warn("Password reset token already used", zap.String("user_id", userID.String()), zap.String("token", tokenPreview), ) return uuid.Nil, fmt.Errorf("token already used") } if time.Now().After(expiresAt) { s.logger.Warn("Password reset token expired", zap.String("user_id", userID.String()), zap.Time("expires_at", expiresAt), ) return uuid.Nil, fmt.Errorf("token expired") } s.logger.Info("Password reset token verified successfully", zap.String("user_id", userID.String()), ) return userID, nil } // MarkTokenAsUsed marque un token comme utilisé // T0192: Marque le token comme utilisé après utilisation func (s *PasswordResetService) MarkTokenAsUsed(token string) error { ctx := context.Background() result, err := s.db.ExecContext(ctx, "UPDATE password_reset_tokens SET used = TRUE WHERE token = $1", token, ) if err != nil { s.logger.Error("Failed to mark token as used", zap.String("token", token[:utils.Min(len(token), 8)]+"..."), zap.Error(err), ) return fmt.Errorf("failed to mark token as used: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { s.logger.Warn("Failed to get rows affected", zap.Error(err)) } else if rowsAffected == 0 { s.logger.Warn("No token found to mark as used", zap.String("token", token[:utils.Min(len(token), 8)]+"..."), ) return fmt.Errorf("token not found") } s.logger.Info("Password reset token marked as used", zap.String("token", token[:utils.Min(len(token), 8)]+"..."), ) return nil } // InvalidateOldTokens invalide tous les tokens de réinitialisation précédents pour un utilisateur // T0192: Invalide les tokens précédents pour un utilisateur func (s *PasswordResetService) InvalidateOldTokens(userID uuid.UUID) error { ctx := context.Background() result, err := s.db.ExecContext(ctx, "UPDATE password_reset_tokens SET used = TRUE WHERE user_id = $1 AND used = FALSE", userID, ) if err != nil { s.logger.Error("Failed to invalidate old tokens", zap.String("user_id", userID.String()), zap.Error(err), ) return fmt.Errorf("failed to invalidate old tokens: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { s.logger.Warn("Failed to get rows affected", zap.Error(err)) } else { s.logger.Info("Old password reset tokens invalidated", zap.String("user_id", userID.String()), zap.Int64("tokens_invalidated", rowsAffected), ) } return nil }