On branch main

modified:   internal/database/psql_db.go
	modified:   internal/domain/auth/dto.go
	modified:   internal/domain/auth/handler.go
	modified:   internal/domain/auth/router.go
	modified:   internal/domain/auth/servcie.go
	new file:   internal/models/password_reset.go
	modified:   internal/repository/account_repository.go
	modified:   internal/repository/account_repository_impl.go
auth domain is implemented but not tested
This commit is contained in:
2026-03-31 05:29:46 +05:00
parent 8b40d1bfe5
commit 21c6c03b27
8 changed files with 326 additions and 147 deletions
@@ -5,6 +5,8 @@ import (
"api_yal/internal/logger"
"api_yal/internal/models"
"api_yal/internal/repository"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"time"
@@ -17,12 +19,15 @@ import (
// AuthService интерфейс сервиса аутентификации
type AuthService interface {
Register(req RegisterRequest) (*AuthResponse, error)
Login(req LoginRequest) (*AuthResponse, string, error) // Возвращает refresh token отдельно
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)
GenerateNewRefreshToken(userID uint) (string, error)
// Reset password methods
RequestPasswordReset(email string) (string, error) // Возвращает reset token
ConfirmPasswordReset(token, newPassword string) error
}
// authServiceImpl реализация сервиса аутентификации
@@ -31,6 +36,7 @@ type authServiceImpl struct {
jwtSecret []byte
accessTokenTTL time.Duration
refreshTokenTTL time.Duration
resetTokenTTL time.Duration
}
// AuthServiceConfig конфигурация для сервиса аутентификации
@@ -38,6 +44,7 @@ type AuthServiceConfig struct {
JWTSecret string
AccessTokenTTL time.Duration // Рекомендуется 15-30 минут
RefreshTokenTTL time.Duration // Рекомендуется 7-30 дней
ResetTokenTTL time.Duration // Рекомендуется 1 час
}
// NewAuthService создает новый экземпляр сервиса аутентификации
@@ -47,43 +54,10 @@ func NewAuthService(accountRepo repository.AccountRepository, config AuthService
jwtSecret: []byte(config.JWTSecret),
accessTokenTTL: config.AccessTokenTTL,
refreshTokenTTL: config.RefreshTokenTTL,
resetTokenTTL: config.ResetTokenTTL,
}
}
// GenerateNewRefreshToken генерирует новый refresh token для пользователя по ID
func (s *authServiceImpl) GenerateNewRefreshToken(userID uint) (string, error) {
l := logger.Get()
l.Info("Генерация нового refresh token", zap.Uint("userID", userID))
// Получаем пользователя из базы данных
account, err := s.accountRepo.GetByID(userID)
if err != nil {
l.Error("Пользователь не найден при генерации refresh token",
zap.Uint("userID", userID),
zap.Error(err))
return "", ErrUserNotFound
}
// Проверяем, активен ли аккаунт
if !account.IsActive {
l.Error("Попытка генерации refresh token для деактивированного аккаунта",
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.Uint("userID", userID),
zap.Error(err))
return "", err
}
l.Info("Refresh token успешно сгенерирован", zap.Uint("userID", userID))
return refreshToken, nil
}
// Register регистрирует нового пользователя
func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) {
l := logger.Get()
@@ -147,7 +121,7 @@ func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) {
return response, nil
}
// Login аутентифицирует пользователя и возвращает access и refresh токены
// Login аутентифицирует пользователя
func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, string, error) {
l := logger.Get()
l.Info("Начало входа пользователя", zap.String("email", req.Email))
@@ -200,6 +174,99 @@ func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, string, error)
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(account)
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()
@@ -266,7 +333,6 @@ func (s *authServiceImpl) Logout(userID uint) error {
l := logger.Get()
l.Info("Выход пользователя", zap.Uint("userID", userID))
// Здесь можно добавить логику инвалидации токенов (например, в Redis)
// Для базовой реализации достаточно удалить cookie на клиенте
l.Info("Выход успешно завершен", zap.Uint("userID", userID))
return nil
}
@@ -274,7 +340,6 @@ func (s *authServiceImpl) Logout(userID uint) error {
// 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"])
}
@@ -294,7 +359,6 @@ func (s *authServiceImpl) ValidateAccessToken(tokenString string) (*jwt.MapClaim
return nil, ErrInvalidToken
}
// Проверяем тип токена
if tokenType, exists := claims["type"]; !exists || tokenType != "access" {
return nil, ErrInvalidToken
}
@@ -360,6 +424,18 @@ func (s *authServiceImpl) generateRefreshToken(account *models.Account) (string,
return tokenString, nil
}
// generateResetToken генерирует reset token
func (s *authServiceImpl) generateResetToken(account *models.Account) (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) {
@@ -382,12 +458,10 @@ func (s *authServiceImpl) validateRefreshToken(tokenString string) (*jwt.MapClai
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