modified: serv_nginx/api_bb/.env
modified: serv_nginx/api_bb/go.mod modified: serv_nginx/api_bb/go.sum modified: serv_nginx/api_bb/internal/database/migrate.go modified: serv_nginx/api_bb/internal/handlers/auth.go new file: serv_nginx/api_bb/internal/handlers/email_handler.go modified: serv_nginx/api_bb/internal/handlers/handlers.go modified: serv_nginx/api_bb/internal/models/user.go new file: serv_nginx/api_bb/internal/repository/email_repository.go modified: serv_nginx/api_bb/internal/repository/user_repository.go modified: serv_nginx/api_bb/internal/routes/routes.go new file: serv_nginx/api_bb/internal/service/email_service.go modified: serv_nginx/api_bb/internal/service/user_service.go new file: serv_nginx/api_bb/pkg/email/email.go add email sender, vrificator and reset password
This commit is contained in:
@@ -26,6 +26,7 @@ func (d *Database) Migrate() error {
|
||||
&models.EventRegistration{},
|
||||
&models.PersonalBest{},
|
||||
&models.TrainingPlan{},
|
||||
&models.EmailVerification{},
|
||||
// Добавьте другие модели здесь
|
||||
}
|
||||
|
||||
@@ -96,6 +97,8 @@ func getModelName(model interface{}) string {
|
||||
return "Персональные достижения"
|
||||
case *models.TrainingPlan:
|
||||
return "Тренировочный план"
|
||||
case *models.EmailVerification:
|
||||
return "Верификация email"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
@@ -18,16 +18,18 @@ import (
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
authService service.AuthService
|
||||
jwtService service.JWTService
|
||||
logger logger.LoggerInterface
|
||||
authService service.AuthService
|
||||
jwtService service.JWTService
|
||||
logger logger.LoggerInterface
|
||||
emailService service.EmailService
|
||||
}
|
||||
|
||||
func NewAuthHandler(authService service.AuthService, jwtService service.JWTService) *AuthHandler {
|
||||
func NewAuthHandler(authService service.AuthService, jwtService service.JWTService, emailService service.EmailService) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
authService: authService,
|
||||
jwtService: jwtService,
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "auth"))),
|
||||
authService: authService,
|
||||
jwtService: jwtService,
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "auth"))),
|
||||
emailService: emailService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +138,13 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
zap.String("email", user.Email),
|
||||
)
|
||||
|
||||
// Отправки сообщения для верификации Email
|
||||
if err := h.emailService.SendVerificationEmail(user.ID, user.Email, user.FirstName); err != nil {
|
||||
h.logger.Error("failed to send verification email",
|
||||
zap.Error(err),
|
||||
zap.Uint("user_id", user.ID))
|
||||
}
|
||||
|
||||
// После успешной регистрации возвращаем данные пользователя
|
||||
utils.RespondWithJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"message": "User registered successfully",
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
// handlers/email_handler.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
"api_bb/pkg/utils"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type EmailHandler struct {
|
||||
logger logger.LoggerInterface
|
||||
emailService *service.EmailService
|
||||
}
|
||||
|
||||
func NewEmailHandler(emailService *service.EmailService) *EmailHandler {
|
||||
return &EmailHandler{
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "email"))),
|
||||
emailService: emailService,
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyEmail подтверждает email пользователя
|
||||
func (h *EmailHandler) VerifyEmail(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling email verification request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
token := r.URL.Query().Get("token")
|
||||
if token == "" {
|
||||
h.logger.Warn("email verification failed - token is required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Токен обязателен")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.emailService.VerifyEmail(token); err != nil {
|
||||
h.logger.Error("email verification failed, expired",
|
||||
zap.Error(err),
|
||||
zap.String("token", token),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Неверный или просроченный токен")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("email successfully verified",
|
||||
zap.String("token", token),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Email успешно подтвержден",
|
||||
})
|
||||
}
|
||||
|
||||
// RequestPasswordReset запрашивает сброс пароля
|
||||
func (h *EmailHandler) RequestPasswordReset(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling password reset request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
var req models.PasswordResetRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Warn("password reset request failed - invalid request format",
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Неверный формат запроса")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.emailService.SendPasswordResetEmail(req.Email); err != nil {
|
||||
h.logger.Error("password reset request failed",
|
||||
zap.Error(err),
|
||||
zap.String("email", req.Email),
|
||||
)
|
||||
// Для безопасности всегда возвращаем успех
|
||||
}
|
||||
|
||||
h.logger.Info("password reset request processed",
|
||||
zap.String("email", req.Email),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Если email зарегистрирован, инструкции по восстановлению пароля будут отправлены",
|
||||
})
|
||||
}
|
||||
|
||||
// ConfirmPasswordReset подтверждает сброс пароля
|
||||
func (h *EmailHandler) ConfirmPasswordReset(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling password reset confirmation request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
var req models.PasswordResetConfirm
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Warn("password reset confirmation failed - invalid request format",
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Неверный формат запроса")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.emailService.ResetPassword(req.Token, req.Password); err != nil {
|
||||
h.logger.Error("password reset confirmation failed",
|
||||
zap.Error(err),
|
||||
zap.String("token", req.Token),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Неверный или просроченный токен")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("password successfully reset",
|
||||
zap.String("token", req.Token),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Пароль успешно изменен",
|
||||
})
|
||||
}
|
||||
|
||||
type NewsletterRequest struct {
|
||||
Subject string `json:"subject" validate:"required"`
|
||||
Content string `json:"content" validate:"required"`
|
||||
}
|
||||
|
||||
// SendNewsletter отправляет рассылку новостей
|
||||
func (h *EmailHandler) SendNewsletter(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling newsletter sending request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
var req NewsletterRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Warn("newsletter sending failed - invalid request format",
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Неверный формат запроса")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.emailService.SendNewsletterToSubscribers(req.Subject, req.Content); err != nil {
|
||||
h.logger.Error("newsletter sending failed",
|
||||
zap.Error(err),
|
||||
zap.String("subject", req.Subject),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Не удалось отправить рассылку")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("newsletter sent successfully",
|
||||
zap.String("subject", req.Subject),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Рассылка отправлена подписчикам",
|
||||
})
|
||||
}
|
||||
|
||||
// ResendVerification повторно отправляет email верификации
|
||||
func (h *EmailHandler) ResendVerification(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling resend verification request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("resend verification failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Пользователь не авторизован")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем пользователя
|
||||
userData, err := h.emailService.GetUserByID(user.ID)
|
||||
if err != nil {
|
||||
h.logger.Warn("resend verification failed - user not found",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Пользователь не найден")
|
||||
return
|
||||
}
|
||||
|
||||
if userData.EmailVerified {
|
||||
h.logger.Warn("resend verification failed - email already verified",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", userData.Email),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Email уже подтвержден")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.emailService.SendVerificationEmail(userData.ID, userData.Email, userData.FirstName); err != nil {
|
||||
h.logger.Error("resend verification failed",
|
||||
zap.Error(err),
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", userData.Email),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Не удалось отправить email подтверждения")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("verification email resent successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", userData.Email),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Email подтверждения отправлен повторно",
|
||||
})
|
||||
}
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"api_bb/internal/config"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/email"
|
||||
"api_bb/pkg/logger"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -24,6 +26,7 @@ type Handler struct {
|
||||
eventRegistrationHandler *EventRegistrationHandler
|
||||
personalBestHandler *PersonalBestHandler
|
||||
trainingPlanHandler *TrainingPlanHandler
|
||||
emailHandler *EmailHandler
|
||||
// Здесь будут добавлены другие обработчики
|
||||
// userHandler *UserHandler
|
||||
// eventHandler *EventHandler
|
||||
@@ -43,10 +46,17 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
|
||||
eventRegistrationRepo := repository.NewEventRegistrationRepository(db)
|
||||
personalBestRepo := repository.NewPersonalBestRepository(db)
|
||||
trainingPlanRepo := repository.NewTrainingPlanRepository(db)
|
||||
emailRepo := repository.NewEmailRepository(db)
|
||||
|
||||
// Initialize logger
|
||||
baseLogger := logger.NewWrapper(logger.Get()) // Создаем базовый логгер
|
||||
|
||||
// getConfig
|
||||
emailSender, err := email.NewService(config.Load())
|
||||
if err != nil {
|
||||
baseLogger.Info("error to load config", zap.Error(err))
|
||||
}
|
||||
|
||||
// Инициализация сервисов
|
||||
jwtService := service.NewJWTService(cfg.JWTSecret)
|
||||
authService := service.NewAuthService(userRepo, jwtService, baseLogger)
|
||||
@@ -61,10 +71,11 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
|
||||
eventService := service.NewEventService(eventRepo, eventRegistrationRepo, baseLogger)
|
||||
personalBestService := service.NewPersonalBestService(personalBestRepo, userStatsService)
|
||||
trainingPlanService := service.NewTrainingPlanService(*trainingPlanRepo)
|
||||
emailService := service.NewEmailService(*emailRepo, userRepo, *emailSender)
|
||||
|
||||
// Инициализация обработчиков
|
||||
healthHandler := NewHealthHandler()
|
||||
authHandler := NewAuthHandler(authService, jwtService)
|
||||
authHandler := NewAuthHandler(authService, jwtService, emailService)
|
||||
userHandler := NewUserHandler(&userService)
|
||||
newsHandler := NewNewsHandler(newsService, baseLogger)
|
||||
avatarHandler := NewAvatarHandler(avatarService)
|
||||
@@ -76,6 +87,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
|
||||
eventRegistrationHandler := NewEventRegistrationHandler(eventRegistrationService)
|
||||
personalBestHandler := NewPersonalBestHandler(*personalBestService)
|
||||
trainingPlanHandler := NewTrainingPlanHandler(trainingPlanService)
|
||||
emailHandler := NewEmailHandler(&emailService)
|
||||
|
||||
return &Handler{
|
||||
healthHandler: healthHandler,
|
||||
@@ -91,10 +103,15 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
|
||||
eventRegistrationHandler: eventRegistrationHandler,
|
||||
personalBestHandler: personalBestHandler,
|
||||
trainingPlanHandler: trainingPlanHandler,
|
||||
emailHandler: emailHandler,
|
||||
}
|
||||
}
|
||||
|
||||
// Геттеры для обработчиков (опционально, для удобства)
|
||||
func (h *Handler) EmailHandler() *EmailHandler {
|
||||
return h.emailHandler
|
||||
}
|
||||
|
||||
func (h *Handler) TrainingPlanHandler() *TrainingPlanHandler {
|
||||
return h.trainingPlanHandler
|
||||
}
|
||||
|
||||
@@ -10,20 +10,22 @@ import (
|
||||
|
||||
// models/user.go - добавить поле Avatar
|
||||
type User struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Email string `json:"email" gorm:"uniqueIndex;not null"`
|
||||
Password string `json:"-" gorm:"not null"`
|
||||
FirstName string `json:"first_name" gorm:"not null"`
|
||||
LastName string `json:"last_name" gorm:"not null"`
|
||||
Avatar string `json:"avatar"` // Путь к файлу аватара
|
||||
Phone string `json:"phone"`
|
||||
Experience string `json:"experience"`
|
||||
Goals string `json:"goals"`
|
||||
Newsletter bool `json:"newsletter"`
|
||||
Role string `json:"role" gorm:"default:user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Email string `json:"email" gorm:"uniqueIndex;not null"`
|
||||
Password string `json:"-" gorm:"not null"`
|
||||
FirstName string `json:"first_name" gorm:"not null"`
|
||||
LastName string `json:"last_name" gorm:"not null"`
|
||||
Avatar string `json:"avatar"` // Путь к файлу аватара
|
||||
Phone string `json:"phone"`
|
||||
Experience string `json:"experience"`
|
||||
Goals string `json:"goals"`
|
||||
Newsletter bool `json:"newsletter"`
|
||||
Role string `json:"role" gorm:"default:user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
EmailVerified bool `json:"email_verified" gorm:"default:false"`
|
||||
VerifiedAt time.Time `json:"verified_at"`
|
||||
|
||||
// Связи
|
||||
Workouts []Workout `json:"workouts,omitempty" gorm:"foreignKey:UserID"`
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// repository/email_repository.go
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type EmailRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewEmailRepository(db *gorm.DB) *EmailRepository {
|
||||
return &EmailRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *EmailRepository) CreateVerificationToken(verification *models.EmailVerification) error {
|
||||
return r.db.Create(verification).Error
|
||||
}
|
||||
|
||||
func (r *EmailRepository) GetVerificationToken(token string) (*models.EmailVerification, error) {
|
||||
var verification models.EmailVerification
|
||||
err := r.db.Where("token = ? AND used = ? AND expires_at > ?", token, false, time.Now()).
|
||||
Preload("User").
|
||||
First(&verification).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &verification, nil
|
||||
}
|
||||
|
||||
func (r *EmailRepository) MarkTokenAsUsed(token string) error {
|
||||
return r.db.Model(&models.EmailVerification{}).
|
||||
Where("token = ?", token).
|
||||
Updates(map[string]interface{}{
|
||||
"used": true,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *EmailRepository) DeleteExpiredTokens() error {
|
||||
return r.db.Where("expires_at < ?", time.Now()).Delete(&models.EmailVerification{}).Error
|
||||
}
|
||||
|
||||
func (r *EmailRepository) GetUsersWithNewsletter() ([]models.User, error) {
|
||||
var users []models.User
|
||||
err := r.db.Where("newsletter = ? AND email_verified = ?", true, true).
|
||||
Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
// MarkEmailAsVerified помечает email пользователя как верифицированный
|
||||
func (r *EmailRepository) MarkEmailAsVerified(userID uint) error {
|
||||
return r.db.Model(&models.User{}).
|
||||
Where("id = ?", userID).
|
||||
Updates(map[string]interface{}{
|
||||
"email_verified": true,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// GetUserByEmail возвращает пользователя по email
|
||||
func (r *EmailRepository) GetUserByEmail(email string) (*models.User, error) {
|
||||
var user models.User
|
||||
err := r.db.Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdatePassword обновляет пароль пользователя
|
||||
func (r *EmailRepository) UpdatePassword(userID uint, newPassword string) error {
|
||||
return r.db.Model(&models.User{}).
|
||||
Where("id = ?", userID).
|
||||
Updates(map[string]interface{}{
|
||||
"password": newPassword,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ type UserRepository interface {
|
||||
UpdateExcludeEmail(userUpdate *models.User) error
|
||||
UpdateAvatar(userID uint, avatarPath string) error
|
||||
FindAll() ([]models.User, error)
|
||||
MarkEmailAsVerified(userID uint) error
|
||||
UpdatePassword(userID uint, newPassword string) error
|
||||
GetUserByID(id uint) (*models.User, error)
|
||||
}
|
||||
|
||||
func (r *userRepository) UpdateAvatar(userID uint, avatarPath string) error {
|
||||
@@ -91,3 +94,33 @@ func (r *userRepository) UpdateExcludeEmail(userUpdate *models.User) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkEmailAsVerified помечает email пользователя как верифицированный
|
||||
func (r userRepository) MarkEmailAsVerified(userID uint) error {
|
||||
result := r.db.Model(&models.User{}).Where("id = ?", userID).Update("email_verified", true)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePassword обновляет пароль пользователя
|
||||
func (r userRepository) UpdatePassword(userID uint, newPassword string) error {
|
||||
result := r.db.Model(&models.User{}).Where("id = ?", userID).Update("password", newPassword)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r userRepository) GetUserByID(id uint) (*models.User, error) {
|
||||
var user models.User
|
||||
err := r.db.First(&user, id).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
@@ -34,12 +34,16 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
|
||||
// Initialize logger
|
||||
baseLogger := logger.NewWrapper(logger.Get()) // Создаем базовый логгер
|
||||
baseLogger := logger.NewWrapper(logger.Get())
|
||||
|
||||
// Initialize services with logger
|
||||
jwtService := service.NewJWTService(config.JWTSecret)
|
||||
|
||||
// Initialize handlers
|
||||
// Email service initialization with fallback
|
||||
var emailHandler *handlers.EmailHandler
|
||||
if h.EmailHandler() != nil {
|
||||
emailHandler = h.EmailHandler()
|
||||
}
|
||||
|
||||
// Health routes
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
@@ -49,12 +53,24 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
||||
|
||||
// API v1 routes
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
|
||||
// Email verification (public) - только если доступен
|
||||
if emailHandler != nil {
|
||||
r.Get("/verify-email", emailHandler.VerifyEmail)
|
||||
}
|
||||
|
||||
// Public auth routes
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Post("/register", h.AuthHandler().Register)
|
||||
r.Post("/login", h.AuthHandler().Login)
|
||||
r.Post("/logout", h.AuthHandler().Logout)
|
||||
|
||||
// Email routes (only if email handler is available)
|
||||
if emailHandler != nil {
|
||||
r.Post("/verify-email/resend", emailHandler.ResendVerification)
|
||||
r.Post("/password-reset/request", emailHandler.RequestPasswordReset)
|
||||
r.Post("/password-reset/confirm", emailHandler.ConfirmPasswordReset)
|
||||
}
|
||||
})
|
||||
|
||||
// Публичные маршруты для достижений (если нужны)
|
||||
@@ -238,6 +254,7 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
||||
})
|
||||
})
|
||||
|
||||
// Events
|
||||
r.Route("/events", func(r chi.Router) {
|
||||
|
||||
// Публичные маршруты
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
// 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
|
||||
}
|
||||
@@ -135,3 +135,5 @@ func (s *userService) GetUserProfile(userID uint) (*models.User, error) {
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user