veza/veza-backend-api/internal/services/password_reset_service.go
2025-12-03 20:29:37 +01:00

185 lines
5.5 KiB
Go

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
}