298 lines
8.3 KiB
Go
298 lines
8.3 KiB
Go
// 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
|
|
}
|