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:
@@ -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,20 +70,21 @@ 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,
|
||||||
ExpiresAt: expiresAt,
|
RefreshToken: refreshToken,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
User: UserInfo{
|
User: UserInfo{
|
||||||
ID: newAcc.Base.ID,
|
ID: newAcc.Base.ID,
|
||||||
Email: newAcc.Email,
|
Email: newAcc.Email,
|
||||||
@@ -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,35 +107,33 @@ 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,
|
||||||
ExpiresAt: expiresAt,
|
RefreshToken: refreshToken,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
User: UserInfo{
|
User: UserInfo{
|
||||||
ID: account.Base.ID,
|
ID: account.Base.ID,
|
||||||
Email: account.Email,
|
Email: account.Email,
|
||||||
@@ -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,
|
||||||
ExpiresAt: expiresAt,
|
RefreshToken: newRefreshToken,
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Refresh token expires in 7 days
|
||||||
|
refreshExpiresAt := time.Now().Add(7 * 24 * time.Hour)
|
||||||
|
|
||||||
// Создаем claims
|
// Создаем access token
|
||||||
claims := jwt.MapClaims{
|
accessClaims := jwt.MapClaims{
|
||||||
"sub": account.Base.ID,
|
"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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user