On branch main

modified:   internal/domain/auth/handler.go
	modified:   internal/domain/auth/servcie.go
Refresh token is upadateble from now
This commit is contained in:
2026-03-31 03:24:07 +05:00
parent 8c63b1fbb9
commit 659cd3584c
2 changed files with 124 additions and 81 deletions
@@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"api_yal/internal/logger" "api_yal/internal/logger"
"api_yal/internal/middleware" "api_yal/internal/middleware"
@@ -83,7 +82,7 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
// Login вход пользователя // Login вход пользователя
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
l := logger.Get() l := logger.Get()
l.Info("Начало обработки запроса входа") l.Debug("Начало обработки запроса входа")
var req LoginRequest var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -128,7 +127,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
return return
} }
l.Info("Завершение обработки запроса входа") l.Debug("Завершение обработки запроса входа")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
} }
@@ -138,23 +137,20 @@ func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
l := logger.Get() l := logger.Get()
l.Info("Начало обработки запроса обновления токена") l.Info("Начало обработки запроса обновления токена")
// Получаем токен из заголовка Authorization var req RefreshTokenRequest
authHeader := r.Header.Get("Authorization") if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
if authHeader == "" { http.Error(w, "Invalid request body", http.StatusBadRequest)
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return return
} }
// Ожидаем формат "Bearer <token>" if err := h.validator.Struct(req); err != nil {
parts := strings.Split(authHeader, " ") http.Error(w, "Invalid request", http.StatusBadRequest)
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
return return
} }
response, err := h.authService.RefreshToken(parts[1]) response, err := h.authService.RefreshToken(req.RefreshToken)
if err != nil { if err != nil {
l.Error("Ошибка обновления токена: %v", zap.Error(err)) l.Error("Ошибка обновления токена", zap.Error(err))
http.Error(w, "Token refresh failed", http.StatusUnauthorized) http.Error(w, "Token refresh failed", http.StatusUnauthorized)
return return
} }
@@ -5,6 +5,7 @@ import (
"api_yal/internal/models" "api_yal/internal/models"
"api_yal/internal/repository" "api_yal/internal/repository"
"errors" "errors"
"fmt"
"time" "time"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
@@ -13,14 +14,10 @@ import (
) )
// AuthService интерфейс сервиса аутентификации // AuthService интерфейс сервиса аутентификации
// Register регистрирует нового пользователя
// Login аутентифицирует пользователя
// RefreshToken обновляет JWT токен
// Logout завершает сессию пользователя
type AuthService interface { type AuthService interface {
Register(req RegisterRequest) (*AuthResponse, error) Register(req RegisterRequest) (*AuthResponse, error)
Login(req LoginRequest) (*AuthResponse, error) Login(req LoginRequest) (*AuthResponse, error)
RefreshToken(token string) (*AuthResponse, error) RefreshToken(refreshToken string) (*AuthResponse, error)
Logout(userID uint) error Logout(userID uint) error
} }
@@ -41,7 +38,7 @@ func NewAuthService(accountRepo repository.AccountRepository, jwtSecret string)
// Register регистрирует нового пользователя // Register регистрирует нового пользователя
func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) { func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) {
l := logger.Get() l := logger.Get()
l.Info("Начало регистрации нового пользователя", zap.String("email", req.Email), zap.String("first_name", req.FirstName), zap.String("last_name", req.LastName)) l.Info("Начало регистрации нового пользователя", zap.String("email", req.Email))
// Проверяем, существует ли пользователь с таким email // Проверяем, существует ли пользователь с таким email
existingUser, err := s.accountRepo.GetByEmail(req.Email) existingUser, err := s.accountRepo.GetByEmail(req.Email)
@@ -52,7 +49,7 @@ func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) {
// Хешируем пароль // Хешируем пароль
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {
l.Error("Ошибка хеширования пароля: %v", zap.Error(err)) l.Error("Ошибка хеширования пароля", zap.Error(err))
return nil, err return nil, err
} }
@@ -73,19 +70,20 @@ func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) {
// Сохраняем в базу данных // Сохраняем в базу данных
if err := s.accountRepo.Create(newAcc); err != nil { if err := s.accountRepo.Create(newAcc); err != nil {
l.Error("Ошибка создания аккаунта: %v", zap.Error(err)) l.Error("Ошибка создания аккаунта", zap.Error(err))
return nil, err return nil, err
} }
// Генерируем JWT токен // Генерируем JWT токены
token, expiresAt, err := s.generateToken(newAcc) accessToken, refreshToken, expiresAt, err := s.generateTokens(newAcc)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Формируем ответ // Формируем ответ
response := &AuthResponse{ response := &AuthResponse{
Token: token, Token: accessToken,
RefreshToken: refreshToken,
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
User: UserInfo{ User: UserInfo{
ID: newAcc.Base.ID, ID: newAcc.Base.ID,
@@ -97,8 +95,7 @@ func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) {
}, },
} }
l.Info("Пользователь успешно зарегистрирован: %s", zap.String("Email", req.Email)) l.Info("Пользователь успешно зарегистрирован", zap.String("email", req.Email))
l.Info("Регистрация успешно завершена", zap.String("email", req.Email))
return response, nil return response, nil
} }
@@ -110,34 +107,32 @@ func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, error) {
// Ищем пользователя по email // Ищем пользователя по email
account, err := s.accountRepo.GetByEmail(req.Email) account, err := s.accountRepo.GetByEmail(req.Email)
if err != nil { if err != nil {
l.Error("Пользователь не найден:", l.Error("Пользователь не найден", zap.String("email", req.Email), zap.Error(err))
zap.String("Email", req.Email),
zap.Error(err),
)
return nil, ErrUserNotFound return nil, ErrUserNotFound
} }
// Проверяем, активен ли аккаунт // Проверяем, активен ли аккаунт
if !account.IsActive { if !account.IsActive {
l.Error("Аккаунт деактивирован: %s", zap.String("Email", req.Email)) l.Error("Аккаунт деактивирован", zap.String("email", req.Email))
return nil, errors.New("account is deactivated") return nil, errors.New("account is deactivated")
} }
// Сравниваем пароли // Сравниваем пароли
if err := bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(req.Password)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(req.Password)); err != nil {
l.Error("Неверный пароль для пользователя: %s", zap.String("Email", req.Email)) l.Error("Неверный пароль для пользователя", zap.String("email", req.Email))
return nil, ErrInvalidPassword return nil, ErrInvalidPassword
} }
// Генерируем JWT токен // Генерируем JWT токены
token, expiresAt, err := s.generateToken(account) accessToken, refreshToken, expiresAt, err := s.generateTokens(account)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Формируем ответ // Формируем ответ
response := &AuthResponse{ response := &AuthResponse{
Token: token, Token: accessToken,
RefreshToken: refreshToken,
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
User: UserInfo{ User: UserInfo{
ID: account.Base.ID, ID: account.Base.ID,
@@ -149,87 +144,139 @@ func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, error) {
}, },
} }
l.Info("Пользователь успешно вошел: %s", zap.String("Email", req.Email)) l.Info("Пользователь успешно вошел", zap.String("email", req.Email))
l.Info("Вход успешно завершен", zap.String("email", req.Email))
return response, nil return response, nil
} }
// RefreshToken обновляет JWT токен // RefreshToken обновляет JWT токен по refresh token
func (s *authServiceImpl) RefreshToken(token string) (*AuthResponse, error) { func (s *authServiceImpl) RefreshToken(refreshToken string) (*AuthResponse, error) {
l := logger.Get() l := logger.Get()
l.Info("Начало обновления токена") l.Info("Начало обновления токена")
// Парсим и валидируем токен // Парсим и валидируем refresh token
claims := &jwt.RegisteredClaims{} token, err := jwt.Parse(refreshToken, func(t *jwt.Token) (interface{}, error) {
parsedToken, err := jwt.ParseWithClaims(token, claims, 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 return s.jwtSecret, nil
}) })
if err != nil || !parsedToken.Valid { if err != nil || !token.Valid {
l.Error("Невалидный токен для обновления: %v", zap.Error(err)) l.Error("Невалидный refresh token", zap.Error(err))
return nil, errors.New("invalid token") return nil, errors.New("invalid refresh token")
} }
// Получаем ID пользователя из claims // Получаем claims
userID, err := claims.GetSubject() claims, ok := token.Claims.(jwt.MapClaims)
if err != nil { if !ok {
l.Error("Не удалось получить claims из токена")
return nil, errors.New("invalid token claims") return nil, errors.New("invalid token claims")
} }
// Получаем пользователя из базы // Проверяем тип токена (должен быть refresh)
var account *models.Account if tokenType, exists := claims["type"]; !exists || tokenType != "refresh" {
// Здесь нужно преобразовать string в uint l.Error("Токен не является refresh токеном")
// В реальном проекте нужно добавить метод GetByIDString или аналогичный return nil, errors.New("invalid token type")
// Для простоты используем существующий метод }
// account, err = s.accountRepo.GetByEmail(???)
// Временное решение - нужно добавить метод GetByID // Получаем ID пользователя из claims
// Пока пропускаем для демонстрации userIDStr, ok := claims["sub"].(string)
_ = userID if !ok {
l.Error("Не удалось получить subject из токена")
return nil, errors.New("invalid token claims")
}
// Генерируем новый токен // Конвертируем string в uint
newToken, expiresAt, err := s.generateToken(account) var userID uint
if _, err := fmt.Sscan(userIDStr, &userID); err != nil {
l.Error("Ошибка конвертации user ID", zap.Error(err))
return nil, errors.New("invalid user id in token")
}
// Получаем пользователя из базы по ID
account, err := s.accountRepo.GetByID(userID)
if err != nil { 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")
}
// Генерируем новую пару токенов
accessToken, newRefreshToken, expiresAt, err := s.generateTokens(account)
if err != nil {
l.Error("Ошибка генерации токенов", zap.Error(err))
return nil, err return nil, err
} }
l.Info("Обновление токена успешно завершено") l.Info("Обновление токена успешно завершено", zap.Uint("userID", userID))
return &AuthResponse{ return &AuthResponse{
Token: newToken, Token: accessToken,
RefreshToken: newRefreshToken,
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
User: UserInfo{
ID: account.Base.ID,
Email: account.Email,
FirstName: account.FirstName,
LastName: account.LastName,
FullName: account.FullName,
Role: account.Role,
},
}, nil }, nil
} }
// Logout выход пользователя // Logout выход пользователя
func (s *authServiceImpl) Logout(userID uint) error { func (s *authServiceImpl) Logout(userID uint) error {
l := logger.Get() l := logger.Get()
l.Info("Начало выхода пользователя", zap.Uint("userID", userID)) l.Info("Выход пользователя", zap.Uint("userID", userID))
// Здесь можно добавить логику инвалидации токенов (например, в Redis)
l.Info("Выход успешно завершен", zap.Uint("userID", userID)) l.Info("Выход успешно завершен", zap.Uint("userID", userID))
return nil return nil
} }
// generateToken генерирует JWT токен для пользователя // generateTokens генерирует пару токенов: access и refresh
func (s *authServiceImpl) generateToken(account *models.Account) (string, time.Time, error) { func (s *authServiceImpl) generateTokens(account *models.Account) (accessToken, refreshToken string, expiresAt time.Time, err error) {
// Устанавливаем время истечения (24 часа) // Access token expires in 15 minutes (более безопасно для access token)
expiresAt := time.Now().Add(24 * time.Hour) accessExpiresAt := time.Now().Add(15 * time.Minute)
// Создаем claims // Refresh token expires in 7 days
claims := jwt.MapClaims{ refreshExpiresAt := time.Now().Add(7 * 24 * time.Hour)
"sub": account.Base.ID,
// Создаем access token
accessClaims := jwt.MapClaims{
"sub": fmt.Sprintf("%d", account.Base.ID),
"email": account.Email, "email": account.Email,
"role": account.Role, "role": account.Role,
"exp": expiresAt.Unix(), "exp": accessExpiresAt.Unix(),
"iat": time.Now().Unix(), "iat": time.Now().Unix(),
"type": "access",
} }
// Создаем токен accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) accessToken, err = accessTokenObj.SignedString(s.jwtSecret)
// Подписываем токен
tokenString, err := token.SignedString(s.jwtSecret)
if err != nil { if err != nil {
return "", time.Time{}, err return "", "", time.Time{}, err
} }
return tokenString, expiresAt, nil // Создаем refresh token
refreshClaims := jwt.MapClaims{
"sub": fmt.Sprintf("%d", account.Base.ID),
"exp": refreshExpiresAt.Unix(),
"iat": time.Now().Unix(),
"type": "refresh",
"jti": fmt.Sprintf("refresh-%d-%d", account.Base.ID, time.Now().Unix()),
}
refreshTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refreshToken, err = refreshTokenObj.SignedString(s.jwtSecret)
if err != nil {
return "", "", time.Time{}, err
}
return accessToken, refreshToken, accessExpiresAt, nil
} }