diff --git a/main_dc/yalarba/api_yal/internal/domain/auth/handler.go b/main_dc/yalarba/api_yal/internal/domain/auth/handler.go index 4a7d2b2..037a6ac 100644 --- a/main_dc/yalarba/api_yal/internal/domain/auth/handler.go +++ b/main_dc/yalarba/api_yal/internal/domain/auth/handler.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "net/http" - "strings" "api_yal/internal/logger" "api_yal/internal/middleware" @@ -83,7 +82,7 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) { // Login вход пользователя func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { l := logger.Get() - l.Info("Начало обработки запроса входа") + l.Debug("Начало обработки запроса входа") var req LoginRequest 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 } - l.Info("Завершение обработки запроса входа") + l.Debug("Завершение обработки запроса входа") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } @@ -138,23 +137,20 @@ func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) { l := logger.Get() l.Info("Начало обработки запроса обновления токена") - // Получаем токен из заголовка Authorization - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - http.Error(w, "Authorization header required", http.StatusUnauthorized) + var req RefreshTokenRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) return } - // Ожидаем формат "Bearer " - parts := strings.Split(authHeader, " ") - if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { - http.Error(w, "Invalid authorization header format", http.StatusUnauthorized) + if err := h.validator.Struct(req); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) return } - response, err := h.authService.RefreshToken(parts[1]) + response, err := h.authService.RefreshToken(req.RefreshToken) if err != nil { - l.Error("Ошибка обновления токена: %v", zap.Error(err)) + l.Error("Ошибка обновления токена", zap.Error(err)) http.Error(w, "Token refresh failed", http.StatusUnauthorized) return } diff --git a/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go b/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go index 82b92a3..b3ddb40 100644 --- a/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go +++ b/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go @@ -5,6 +5,7 @@ import ( "api_yal/internal/models" "api_yal/internal/repository" "errors" + "fmt" "time" "github.com/golang-jwt/jwt/v5" @@ -13,14 +14,10 @@ import ( ) // AuthService интерфейс сервиса аутентификации -// Register регистрирует нового пользователя -// Login аутентифицирует пользователя -// RefreshToken обновляет JWT токен -// Logout завершает сессию пользователя type AuthService interface { Register(req RegisterRequest) (*AuthResponse, error) Login(req LoginRequest) (*AuthResponse, error) - RefreshToken(token string) (*AuthResponse, error) + RefreshToken(refreshToken string) (*AuthResponse, error) Logout(userID uint) error } @@ -41,7 +38,7 @@ func NewAuthService(accountRepo repository.AccountRepository, jwtSecret string) // Register регистрирует нового пользователя func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) { 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 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) if err != nil { - l.Error("Ошибка хеширования пароля: %v", zap.Error(err)) + l.Error("Ошибка хеширования пароля", zap.Error(err)) return nil, err } @@ -73,20 +70,21 @@ func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) { // Сохраняем в базу данных if err := s.accountRepo.Create(newAcc); err != nil { - l.Error("Ошибка создания аккаунта: %v", zap.Error(err)) + l.Error("Ошибка создания аккаунта", zap.Error(err)) return nil, err } - // Генерируем JWT токен - token, expiresAt, err := s.generateToken(newAcc) + // Генерируем JWT токены + accessToken, refreshToken, expiresAt, err := s.generateTokens(newAcc) if err != nil { return nil, err } // Формируем ответ response := &AuthResponse{ - Token: token, - ExpiresAt: expiresAt, + Token: accessToken, + RefreshToken: refreshToken, + ExpiresAt: expiresAt, User: UserInfo{ ID: newAcc.Base.ID, 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 } @@ -110,35 +107,33 @@ func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, error) { // Ищем пользователя по email account, err := s.accountRepo.GetByEmail(req.Email) if err != nil { - l.Error("Пользователь не найден:", - zap.String("Email", req.Email), - zap.Error(err), - ) + l.Error("Пользователь не найден", zap.String("email", req.Email), zap.Error(err)) return nil, ErrUserNotFound } // Проверяем, активен ли аккаунт 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") } // Сравниваем пароли 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 } - // Генерируем JWT токен - token, expiresAt, err := s.generateToken(account) + // Генерируем JWT токены + accessToken, refreshToken, expiresAt, err := s.generateTokens(account) if err != nil { return nil, err } // Формируем ответ response := &AuthResponse{ - Token: token, - ExpiresAt: expiresAt, + Token: accessToken, + RefreshToken: refreshToken, + ExpiresAt: expiresAt, User: UserInfo{ ID: account.Base.ID, 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 } -// RefreshToken обновляет JWT токен -func (s *authServiceImpl) RefreshToken(token string) (*AuthResponse, error) { +// RefreshToken обновляет JWT токен по refresh token +func (s *authServiceImpl) RefreshToken(refreshToken string) (*AuthResponse, error) { l := logger.Get() l.Info("Начало обновления токена") - // Парсим и валидируем токен - claims := &jwt.RegisteredClaims{} - parsedToken, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) { + // Парсим и валидируем refresh token + token, err := jwt.Parse(refreshToken, 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 || !parsedToken.Valid { - l.Error("Невалидный токен для обновления: %v", zap.Error(err)) - return nil, errors.New("invalid token") + if err != nil || !token.Valid { + l.Error("Невалидный refresh token", zap.Error(err)) + return nil, errors.New("invalid refresh token") } - // Получаем ID пользователя из claims - userID, err := claims.GetSubject() - if err != nil { + // Получаем claims + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + l.Error("Не удалось получить claims из токена") return nil, errors.New("invalid token claims") } - // Получаем пользователя из базы - var account *models.Account - // Здесь нужно преобразовать string в uint - // В реальном проекте нужно добавить метод GetByIDString или аналогичный - // Для простоты используем существующий метод - // account, err = s.accountRepo.GetByEmail(???) + // Проверяем тип токена (должен быть refresh) + if tokenType, exists := claims["type"]; !exists || tokenType != "refresh" { + l.Error("Токен не является refresh токеном") + return nil, errors.New("invalid token type") + } - // Временное решение - нужно добавить метод GetByID - // Пока пропускаем для демонстрации - _ = userID + // Получаем ID пользователя из claims + userIDStr, ok := claims["sub"].(string) + if !ok { + l.Error("Не удалось получить subject из токена") + return nil, errors.New("invalid token claims") + } - // Генерируем новый токен - newToken, expiresAt, err := s.generateToken(account) + // Конвертируем string в uint + 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 { + 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 } - l.Info("Обновление токена успешно завершено") + l.Info("Обновление токена успешно завершено", zap.Uint("userID", userID)) return &AuthResponse{ - Token: newToken, - ExpiresAt: expiresAt, + Token: accessToken, + 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 } // Logout выход пользователя func (s *authServiceImpl) Logout(userID uint) error { l := logger.Get() - l.Info("Начало выхода пользователя", zap.Uint("userID", userID)) + l.Info("Выход пользователя", zap.Uint("userID", userID)) + // Здесь можно добавить логику инвалидации токенов (например, в Redis) l.Info("Выход успешно завершен", zap.Uint("userID", userID)) return nil } -// generateToken генерирует JWT токен для пользователя -func (s *authServiceImpl) generateToken(account *models.Account) (string, time.Time, error) { - // Устанавливаем время истечения (24 часа) - expiresAt := time.Now().Add(24 * time.Hour) +// generateTokens генерирует пару токенов: access и refresh +func (s *authServiceImpl) generateTokens(account *models.Account) (accessToken, refreshToken string, expiresAt time.Time, err error) { + // Access token expires in 15 minutes (более безопасно для access token) + accessExpiresAt := time.Now().Add(15 * time.Minute) + + // Refresh token expires in 7 days + refreshExpiresAt := time.Now().Add(7 * 24 * time.Hour) - // Создаем claims - claims := jwt.MapClaims{ - "sub": account.Base.ID, + // Создаем access token + accessClaims := jwt.MapClaims{ + "sub": fmt.Sprintf("%d", account.Base.ID), "email": account.Email, "role": account.Role, - "exp": expiresAt.Unix(), + "exp": accessExpiresAt.Unix(), "iat": time.Now().Unix(), + "type": "access", } - // Создаем токен - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - // Подписываем токен - tokenString, err := token.SignedString(s.jwtSecret) + accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) + accessToken, err = accessTokenObj.SignedString(s.jwtSecret) 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 +} \ No newline at end of file