package auth import ( "api_yal/internal/logger" "api_yal/internal/models" "api_yal/internal/repository" "crypto/rand" "encoding/base64" "errors" "fmt" "time" "github.com/golang-jwt/jwt/v5" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" ) // AuthService интерфейс сервиса аутентификации type AuthService interface { Register(req RegisterRequest) (*AuthResponse, error) Login(req LoginRequest) (*AuthResponse, string, error) RefreshToken(refreshToken string) (*RefreshTokenResponse, error) Logout(userID uint) error ValidateAccessToken(tokenString string) (*jwt.MapClaims, error) GetUserFromToken(claims *jwt.MapClaims) (*models.Account, error) // Reset password methods RequestPasswordReset(email string) (string, error) // Возвращает reset token ConfirmPasswordReset(token, newPassword string) error GenerateRefreshTokenForUser(userID uint) (string, error) } // authServiceImpl реализация сервиса аутентификации type authServiceImpl struct { accountRepo repository.AccountRepository jwtSecret []byte accessTokenTTL time.Duration refreshTokenTTL time.Duration resetTokenTTL time.Duration } // AuthServiceConfig конфигурация для сервиса аутентификации type AuthServiceConfig struct { JWTSecret string AccessTokenTTL time.Duration // Рекомендуется 15-30 минут RefreshTokenTTL time.Duration // Рекомендуется 7-30 дней ResetTokenTTL time.Duration // Рекомендуется 1 час } // NewAuthService создает новый экземпляр сервиса аутентификации func NewAuthService(accountRepo repository.AccountRepository, config AuthServiceConfig) AuthService { return &authServiceImpl{ accountRepo: accountRepo, jwtSecret: []byte(config.JWTSecret), accessTokenTTL: config.AccessTokenTTL, refreshTokenTTL: config.RefreshTokenTTL, resetTokenTTL: config.ResetTokenTTL, } } // Register регистрирует нового пользователя func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) { l := logger.Get() l.Info("Начало регистрации нового пользователя", zap.String("email", req.Email)) // Проверяем, существует ли пользователь с таким email existingUser, err := s.accountRepo.GetByEmail(req.Email) if err == nil && existingUser != nil { return nil, ErrUserAlreadyExists } // Хешируем пароль hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { l.Error("Ошибка хеширования пароля", zap.Error(err)) return nil, err } // Формируем полное имя fullName := req.FirstName + " " + req.LastName // Создаем аккаунт newAcc := &models.Account{ Email: req.Email, PasswordHash: string(hashedPassword), FirstName: req.FirstName, LastName: req.LastName, FullName: fullName, IsActive: true, IsVerified: false, Role: "user", } // Сохраняем в базу данных if err := s.accountRepo.Create(newAcc); err != nil { l.Error("Ошибка создания аккаунта", zap.Error(err)) return nil, err } // Генерируем access token accessToken, expiresAt, err := s.generateAccessToken(newAcc) if err != nil { return nil, err } // Формируем ответ response := &AuthResponse{ Token: accessToken, ExpiresAt: expiresAt, User: UserInfo{ ID: newAcc.Base.ID, Email: newAcc.Email, FirstName: newAcc.FirstName, LastName: newAcc.LastName, FullName: newAcc.FullName, Role: newAcc.Role, }, } l.Info("Пользователь успешно зарегистрирован", zap.String("email", req.Email)) return response, nil } // Login аутентифицирует пользователя func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, string, error) { l := logger.Get() l.Info("Начало входа пользователя", zap.String("email", req.Email)) // Ищем пользователя по email account, err := s.accountRepo.GetByEmail(req.Email) if err != nil { l.Error("Пользователь не найден", zap.String("email", req.Email), zap.Error(err)) return nil, "", ErrUserNotFound } // Проверяем, активен ли аккаунт if !account.IsActive { l.Error("Аккаунт деактивирован", zap.String("email", req.Email)) return nil, "", errors.New("account is deactivated") } // Сравниваем пароли if err := bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(req.Password)); err != nil { l.Error("Неверный пароль для пользователя", zap.String("email", req.Email)) return nil, "", ErrInvalidPassword } // Генерируем токены accessToken, expiresAt, err := s.generateAccessToken(account) if err != nil { return nil, "", err } refreshToken, err := s.generateRefreshToken(account) if err != nil { return nil, "", err } // Формируем ответ response := &AuthResponse{ Token: accessToken, ExpiresAt: expiresAt, User: UserInfo{ ID: account.Base.ID, Email: account.Email, FirstName: account.FirstName, LastName: account.LastName, FullName: account.FullName, Role: account.Role, }, } l.Info("Пользователь успешно вошел", zap.String("email", req.Email)) return response, refreshToken, nil } // RequestPasswordReset запрашивает сброс пароля func (s *authServiceImpl) RequestPasswordReset(email string) (string, error) { l := logger.Get() l.Info("Запрос сброса пароля", zap.String("email", email)) // Проверяем существование пользователя account, err := s.accountRepo.GetByEmail(email) if err != nil { l.Error("Пользователь не найден", zap.String("email", email), zap.Error(err)) return "", ErrUserNotFound } // Генерируем reset token resetToken, err := s.generateResetToken() if err != nil { l.Error("Ошибка генерации reset token", zap.Error(err)) return "", err } // Сохраняем reset token в базу данных // Для этого нужно создать модель PasswordReset passwordReset := &models.PasswordReset{ AccountID: account.Base.ID, Token: resetToken, ExpiresAt: time.Now().Add(s.resetTokenTTL), Used: false, } if err := s.accountRepo.CreatePasswordReset(passwordReset); err != nil { l.Error("Ошибка сохранения reset token", zap.Error(err)) return "", err } l.Info("Reset token создан", zap.String("email", email), zap.String("token", resetToken)) return resetToken, nil } // ConfirmPasswordReset подтверждает сброс пароля func (s *authServiceImpl) ConfirmPasswordReset(token, newPassword string) error { l := logger.Get() l.Info("Подтверждение сброса пароля") // Находим reset token в базе passwordReset, err := s.accountRepo.GetPasswordResetByToken(token) if err != nil { l.Error("Reset token не найден", zap.Error(err)) return ErrResetTokenNotFound } // Проверяем, не истек ли токен if passwordReset.ExpiresAt.Before(time.Now()) { l.Error("Reset token истек") return ErrResetTokenInvalid } // Проверяем, не использован ли токен if passwordReset.Used { l.Error("Reset token уже использован") return ErrResetTokenInvalid } // Получаем пользователя account, err := s.accountRepo.GetByID(passwordReset.AccountID) if err != nil { l.Error("Пользователь не найден", zap.Error(err)) return ErrUserNotFound } // Хешируем новый пароль hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) if err != nil { l.Error("Ошибка хеширования пароля", zap.Error(err)) return err } // Обновляем пароль пользователя account.PasswordHash = string(hashedPassword) if err := s.accountRepo.Update(account); err != nil { l.Error("Ошибка обновления пароля", zap.Error(err)) return err } // Помечаем токен как использованный passwordReset.Used = true if err := s.accountRepo.UpdatePasswordReset(passwordReset); err != nil { l.Error("Ошибка обновления статуса токена", zap.Error(err)) // Не возвращаем ошибку, так как пароль уже изменен } l.Info("Пароль успешно изменен", zap.Uint("userID", account.Base.ID)) return nil } // RefreshToken обновляет JWT токен по refresh token func (s *authServiceImpl) RefreshToken(refreshToken string) (*RefreshTokenResponse, error) { l := logger.Get() l.Info("Начало обновления токена") // Парсим и валидируем refresh token claims, err := s.validateRefreshToken(refreshToken) if err != nil { l.Error("Ошибка валидации refresh token", zap.Error(err)) return nil, err } // Получаем ID пользователя из claims userIDStr, ok := (*claims)["sub"].(string) if !ok { l.Error("Не удалось получить subject из токена") return nil, ErrInvalidToken } // Конвертируем string в uint var userID uint if _, err := fmt.Sscan(userIDStr, &userID); err != nil { l.Error("Ошибка конвертации user ID", zap.Error(err)) return nil, ErrInvalidToken } // Получаем пользователя из базы по ID account, err := s.accountRepo.GetByID(userID) if err != nil { l.Error("Пользователь не найден", zap.Uint("userID", userID), zap.Error(err)) return nil, ErrUserNotFound } // Проверяем, активен ли аккаунт if !account.IsActive { l.Error("Аккаунт деактивирован", zap.Uint("userID", userID)) return nil, errors.New("account is deactivated") } // Генерируем новый access token accessToken, expiresAt, err := s.generateAccessToken(account) if err != nil { l.Error("Ошибка генерации access token", zap.Error(err)) return nil, err } l.Info("Обновление токена успешно завершено", zap.Uint("userID", userID)) return &RefreshTokenResponse{ Token: accessToken, ExpiresAt: expiresAt, User: UserInfo{ ID: account.Base.ID, Email: account.Email, FirstName: account.FirstName, LastName: account.LastName, FullName: account.FullName, Role: account.Role, }, }, nil } // Logout выход пользователя func (s *authServiceImpl) Logout(userID uint) error { l := logger.Get() l.Info("Выход пользователя", zap.Uint("userID", userID)) // Здесь можно добавить логику инвалидации токенов (например, в Redis) l.Info("Выход успешно завершен", zap.Uint("userID", userID)) return nil } // ValidateAccessToken валидирует access token func (s *authServiceImpl) ValidateAccessToken(tokenString string) (*jwt.MapClaims, error) { token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } return s.jwtSecret, nil }) if err != nil { return nil, err } if !token.Valid { return nil, ErrInvalidToken } claims, ok := token.Claims.(jwt.MapClaims) if !ok { return nil, ErrInvalidToken } if tokenType, exists := claims["type"]; !exists || tokenType != "access" { return nil, ErrInvalidToken } return &claims, nil } // GetUserFromToken получает пользователя из claims func (s *authServiceImpl) GetUserFromToken(claims *jwt.MapClaims) (*models.Account, error) { userIDStr, ok := (*claims)["sub"].(string) if !ok { return nil, ErrInvalidToken } var userID uint if _, err := fmt.Sscan(userIDStr, &userID); err != nil { return nil, ErrInvalidToken } return s.accountRepo.GetByID(userID) } // generateAccessToken генерирует access token func (s *authServiceImpl) generateAccessToken(account *models.Account) (string, time.Time, error) { expiresAt := time.Now().Add(s.accessTokenTTL) claims := jwt.MapClaims{ "sub": fmt.Sprintf("%d", account.Base.ID), "email": account.Email, "role": account.Role, "exp": expiresAt.Unix(), "iat": time.Now().Unix(), "type": "access", } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString(s.jwtSecret) if err != nil { return "", time.Time{}, err } return tokenString, expiresAt, nil } // generateRefreshToken генерирует refresh token func (s *authServiceImpl) generateRefreshToken(account *models.Account) (string, error) { expiresAt := time.Now().Add(s.refreshTokenTTL) claims := jwt.MapClaims{ "sub": fmt.Sprintf("%d", account.Base.ID), "exp": expiresAt.Unix(), "iat": time.Now().Unix(), "type": "refresh", "jti": fmt.Sprintf("refresh-%d-%d", account.Base.ID, time.Now().Unix()), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString(s.jwtSecret) if err != nil { return "", err } return tokenString, nil } // generateResetToken генерирует reset token func (s *authServiceImpl) generateResetToken() (string, error) { // Генерируем случайный токен bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { return "", err } token := base64.URLEncoding.EncodeToString(bytes) return token, nil } // validateRefreshToken валидирует refresh token func (s *authServiceImpl) validateRefreshToken(tokenString string) (*jwt.MapClaims, error) { token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } return s.jwtSecret, nil }) if err != nil { return nil, err } if !token.Valid { return nil, ErrInvalidToken } claims, ok := token.Claims.(jwt.MapClaims) if !ok { return nil, ErrInvalidToken } if tokenType, exists := claims["type"]; !exists || tokenType != "refresh" { return nil, ErrInvalidToken } exp, ok := claims["exp"].(float64) if ok && int64(exp) < time.Now().Unix() { return nil, ErrTokenExpired } return &claims, nil } // GenerateRefreshTokenForUser генерирует новый refresh token для пользователя по ID func (s *authServiceImpl) GenerateRefreshTokenForUser(userID uint) (string, error) { l := logger.Get() l.Info("Генерация refresh token для пользователя", zap.Uint("userID", userID)) // Получаем пользователя из базы по ID account, err := s.accountRepo.GetByID(userID) if err != nil { l.Error("Пользователь не найден", zap.Uint("userID", userID), zap.Error(err)) return "", ErrUserNotFound } // Проверяем, активен ли аккаунт if !account.IsActive { l.Error("Аккаунт деактивирован", zap.Uint("userID", userID)) return "", errors.New("account is deactivated") } // Генерируем refresh token refreshToken, err := s.generateRefreshToken(account) if err != nil { l.Error("Ошибка генерации refresh token", zap.Error(err)) return "", err } l.Info("Refresh token успешно сгенерирован", zap.Uint("userID", userID)) return refreshToken, nil }