// service/email_service.go package service import ( "fmt" "time" "api_bb/internal/models" "api_bb/internal/repository" "api_bb/pkg/email" "api_bb/pkg/logger" "github.com/google/uuid" "go.uber.org/zap" ) type EmailService struct { emailRepo repository.EmailRepository userRepo repository.UserRepository emailSender email.Service logger *zap.Logger tokenExpiry time.Duration passwordExpiry time.Duration } func NewEmailService( emailRepo repository.EmailRepository, userRepo repository.UserRepository, emailSender email.Service, ) EmailService { // Создаем логгер с контекстом для сервиса serviceLogger := logger.Get().With(zap.String("service", "email")) return EmailService{ emailRepo: emailRepo, userRepo: userRepo, emailSender: emailSender, logger: serviceLogger, tokenExpiry: 24 * time.Hour, // 24 часа для верификации passwordExpiry: 1 * time.Hour, // 1 час для сброса пароля } } func (s *EmailService) SendVerificationEmail(userID uint, email, userName string) error { s.logger.Info("Sending verification email", zap.Uint("user_id", userID), zap.String("email", email), ) token := uuid.New().String() verification := &models.EmailVerification{ UserID: userID, Token: token, Email: email, Type: "verification", ExpiresAt: time.Now().Add(s.tokenExpiry), } if err := s.emailRepo.CreateVerificationToken(verification); err != nil { s.logger.Error("Failed to create verification token", zap.Uint("user_id", userID), zap.String("email", email), zap.Error(err), ) return fmt.Errorf("failed to create verification token: %w", err) } if err := s.emailSender.SendVerificationEmail(email, userName, token); err != nil { s.logger.Error("Failed to send verification email", zap.Uint("user_id", userID), zap.String("email", email), zap.Error(err), ) return fmt.Errorf("failed to send verification email: %w", err) } s.logger.Info("Verification email sent successfully", zap.Uint("user_id", userID), zap.String("email", email)) return nil } func (s *EmailService) VerifyEmail(token string) error { s.logger.Info("Verifying email token", zap.String("token", token), ) verification, err := s.emailRepo.GetVerificationToken(token) if err != nil { s.logger.Error("Invalid or expired verification token", zap.String("token", token), zap.Error(err), ) return fmt.Errorf("invalid or expired token: %w", err) } if verification.Type != "verification" { s.logger.Error("Invalid token type for email verification", zap.String("token", token), zap.String("type", verification.Type), ) return fmt.Errorf("invalid token type") } // Обновляем пользователя if err := s.userRepo.MarkEmailAsVerified(verification.UserID); err != nil { s.logger.Error("Failed to verify email in user repository", zap.Uint("user_id", verification.UserID), zap.String("email", verification.Email), zap.Error(err), ) return fmt.Errorf("failed to verify email: %w", err) } // Помечаем токен как использованный if err := s.emailRepo.MarkTokenAsUsed(token); err != nil { s.logger.Error("Failed to mark token as used", zap.Error(err), zap.String("token", token)) } s.logger.Info("Email verified successfully", zap.Uint("user_id", verification.UserID), zap.String("email", verification.Email)) return nil } func (s *EmailService) SendPasswordResetEmail(email string) error { s.logger.Info("Sending password reset email", zap.String("email", email), ) user, err := s.userRepo.FindByEmail(email) if err != nil { // Для безопасности не сообщаем, существует ли email s.logger.Info("Password reset requested for non-existent email", zap.String("email", email)) return nil } token := uuid.New().String() resetRequest := &models.EmailVerification{ UserID: user.ID, Token: token, Email: email, Type: "password_reset", ExpiresAt: time.Now().Add(s.passwordExpiry), } if err := s.emailRepo.CreateVerificationToken(resetRequest); err != nil { s.logger.Error("Failed to create password reset token", zap.Uint("user_id", user.ID), zap.String("email", email), zap.Error(err), ) return fmt.Errorf("failed to create password reset token: %w", err) } if err := s.emailSender.SendPasswordResetEmail(email, user.FirstName, token); err != nil { s.logger.Error("Failed to send password reset email", zap.Uint("user_id", user.ID), zap.String("email", email), zap.Error(err), ) return fmt.Errorf("failed to send password reset email: %w", err) } s.logger.Info("Password reset email sent successfully", zap.Uint("user_id", user.ID), zap.String("email", email)) return nil } func (s *EmailService) ResetPassword(token, newPassword string) error { s.logger.Info("Resetting password with token", zap.String("token", token), ) verification, err := s.emailRepo.GetVerificationToken(token) if err != nil { s.logger.Error("Invalid or expired password reset token", zap.String("token", token), zap.Error(err), ) return fmt.Errorf("invalid or expired token: %w", err) } if verification.Type != "password_reset" { s.logger.Error("Invalid token type for password reset", zap.String("token", token), zap.String("type", verification.Type), ) return fmt.Errorf("invalid token type") } // Обновляем пароль пользователя if err := s.userRepo.UpdatePassword(verification.UserID, newPassword); err != nil { s.logger.Error("Failed to update password", zap.Uint("user_id", verification.UserID), zap.Error(err), ) return fmt.Errorf("failed to update password: %w", err) } // Помечаем токен как использованный if err := s.emailRepo.MarkTokenAsUsed(token); err != nil { s.logger.Error("Failed to mark token as used", zap.Error(err), zap.String("token", token)) } s.logger.Info("Password reset successfully", zap.Uint("user_id", verification.UserID)) return nil } func (s *EmailService) SendNewsletterToSubscribers(subject, content string) error { s.logger.Info("Sending newsletter to subscribers", zap.String("subject", subject), ) subscribers, err := s.emailRepo.GetUsersWithNewsletter() if err != nil { s.logger.Error("Failed to get subscribers", zap.Error(err), ) return fmt.Errorf("failed to get subscribers: %w", err) } s.logger.Debug("Found subscribers for newsletter", zap.Int("count", len(subscribers)), ) var errors []error for _, user := range subscribers { if err := s.emailSender.SendNewsletterEmail(user.Email, user.FirstName, subject, content); err != nil { s.logger.Error("Failed to send newsletter to user", zap.Uint("user_id", user.ID), zap.String("email", user.Email), zap.Error(err)) errors = append(errors, err) continue } s.logger.Debug("Newsletter sent to user", zap.Uint("user_id", user.ID), zap.String("email", user.Email)) } if len(errors) > 0 { s.logger.Error("Failed to send newsletter to some users", zap.Int("failed_count", len(errors)), zap.Int("total_subscribers", len(subscribers)), ) return fmt.Errorf("failed to send newsletter to %d users", len(errors)) } s.logger.Info("Newsletter sent to all subscribers", zap.Int("total_subscribers", len(subscribers))) return nil } func (s *EmailService) CleanupExpiredTokens() error { s.logger.Info("Cleaning up expired tokens") if err := s.emailRepo.DeleteExpiredTokens(); err != nil { s.logger.Error("Failed to cleanup expired tokens", zap.Error(err), ) return fmt.Errorf("failed to cleanup expired tokens: %w", err) } s.logger.Info("Expired tokens cleaned up successfully") return nil } // GetUserByID возвращает пользователя по ID func (s *EmailService) GetUserByID(userID uint) (*models.User, error) { s.logger.Info("Getting user by ID", zap.Uint("user_id", userID), ) user, err := s.userRepo.GetUserByID(userID) if err != nil { s.logger.Error("Failed to get user by ID", zap.Uint("user_id", userID), zap.Error(err), ) return nil, fmt.Errorf("failed to get user: %w", err) } s.logger.Debug("User retrieved successfully", zap.Uint("user_id", userID), zap.String("email", user.Email), ) return user, nil }