On branch main

modified:   internal/domain/auth/dto.go
	modified:   internal/domain/auth/handler.go
	modified:   internal/domain/auth/router.go
	modified:   internal/domain/auth/servcie.go
	modified:   internal/middleware/auth.go
	modified:   internal/router/router.go
auth implemented without reset password
This commit is contained in:
2026-03-31 04:22:54 +05:00
parent 659cd3584c
commit 8b40d1bfe5
6 changed files with 372 additions and 119 deletions
@@ -1,3 +1,4 @@
// dto.go
package auth
import (
@@ -21,9 +22,15 @@ type LoginRequest struct {
// AuthResponse структура ответа при успешной аутентификации
type AuthResponse struct {
Token string `json:"token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
Token string `json:"token"` // Access token для Bearer авторизации
ExpiresAt time.Time `json:"expires_at"` // Время истечения access token
User UserInfo `json:"user"`
}
// RefreshTokenResponse структура ответа при обновлении токена
type RefreshTokenResponse struct {
Token string `json:"token"` // Новый access token
ExpiresAt time.Time `json:"expires_at"` // Время истечения нового access token
User UserInfo `json:"user"`
}
@@ -32,7 +39,7 @@ type ResetPasswordRequest struct {
Email string `json:"email" validate:"required,email"`
}
// RefreshTokenRequest - запрос на обновление токена
// RefreshTokenRequest - запрос на обновление токена (только для мобильных приложений)
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" validate:"required"`
}
@@ -57,4 +64,6 @@ var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidPassword = errors.New("invalid password")
ErrUserAlreadyExists = errors.New("user with this email already exists")
ErrInvalidToken = errors.New("invalid token")
ErrTokenExpired = errors.New("token expired")
)
@@ -1,3 +1,4 @@
// handler.go
package auth
import (
@@ -5,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"time"
"api_yal/internal/logger"
"api_yal/internal/middleware"
@@ -13,6 +15,12 @@ import (
"go.uber.org/zap"
)
// Cookie константы
const (
RefreshTokenCookieName = "refresh_token"
RefreshTokenExpiration = 7 * 24 * time.Hour // 7 дней
)
// AuthHandler обработчик для аутентификации
type AuthHandler struct {
authService AuthService
@@ -20,13 +28,39 @@ type AuthHandler struct {
}
// NewAuthHandler создает новый экземпляр AuthHandler
func NewAuthHandler(authService *AuthService) *AuthHandler {
func NewAuthHandler(authService AuthService) *AuthHandler {
return &AuthHandler{
authService: *authService,
authService: authService,
validator: validator.New(),
}
}
// setRefreshTokenCookie устанавливает HttpOnly cookie с refresh token
func (h *AuthHandler) setRefreshTokenCookie(w http.ResponseWriter, refreshToken string) {
http.SetCookie(w, &http.Cookie{
Name: RefreshTokenCookieName,
Value: refreshToken,
Path: "/",
HttpOnly: true,
Secure: true, // Всегда true в production
SameSite: http.SameSiteStrictMode,
MaxAge: int(RefreshTokenExpiration.Seconds()),
})
}
// clearRefreshTokenCookie удаляет refresh token cookie
func (h *AuthHandler) clearRefreshTokenCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: RefreshTokenCookieName,
Value: "",
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
MaxAge: -1,
})
}
// Register регистрация аккаунта пользователя
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
l := logger.Get()
@@ -110,7 +144,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
return
}
response, err := h.authService.Login(req)
response, refreshToken, err := h.authService.Login(req)
if err != nil {
l.Error("Ошибка входа: %v", zap.Error(err))
@@ -127,39 +161,81 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
return
}
// Устанавливаем refresh token в HttpOnly cookie
h.setRefreshTokenCookie(w, refreshToken)
l.Debug("Завершение обработки запроса входа")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}
// RefreshToken обновление токена
// Поддерживает два способа получения refresh token:
// 1. Из HttpOnly cookie (для web приложений)
// 2. Из тела запроса (для мобильных приложений)
func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
l := logger.Get()
l.Info("Начало обработки запроса обновления токена")
var refreshToken string
// Пытаемся получить refresh token из cookie
cookie, err := r.Cookie(RefreshTokenCookieName)
if err == nil && cookie != nil {
refreshToken = cookie.Value
}
// Если в cookie нет, пробуем получить из тела запроса (для мобильных приложений)
if refreshToken == "" {
var req RefreshTokenRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
if err := json.NewDecoder(r.Body).Decode(&req); err == nil {
refreshToken = req.RefreshToken
}
}
if refreshToken == "" {
http.Error(w, "Refresh token required", http.StatusBadRequest)
return
}
if err := h.validator.Struct(req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
response, err := h.authService.RefreshToken(req.RefreshToken)
response, err := h.authService.RefreshToken(refreshToken)
if err != nil {
l.Error("Ошибка обновления токена", zap.Error(err))
if errors.Is(err, ErrInvalidToken) || errors.Is(err, ErrTokenExpired) {
// Очищаем невалидный refresh token
h.clearRefreshTokenCookie(w)
http.Error(w, "Invalid or expired refresh token", http.StatusUnauthorized)
return
}
http.Error(w, "Token refresh failed", http.StatusUnauthorized)
return
}
// Генерируем новый refresh token
newRefreshToken, err := h.generateNewRefreshTokenFromUser(response.User.ID)
if err != nil {
l.Error("Ошибка генерации нового refresh token", zap.Error(err))
http.Error(w, "Failed to generate refresh token", http.StatusInternalServerError)
return
}
// Обновляем refresh token в cookie
h.setRefreshTokenCookie(w, newRefreshToken)
l.Info("Завершение обработки запроса обновления токена")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}
// generateNewRefreshTokenFromUser генерирует новый refresh token для пользователя
// Вспомогательная функция для обновления refresh token
func (h *AuthHandler) generateNewRefreshTokenFromUser(userID uint) (string, error) {
// Используем сервис для генерации refresh token
return h.authService.GenerateNewRefreshToken(userID)
}
// Logout выход пользователя
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
l := logger.Get()
@@ -178,6 +254,9 @@ func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
return
}
// Очищаем refresh token cookie
h.clearRefreshTokenCookie(w)
l.Info("Завершение обработки запроса выхода")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
@@ -1,9 +1,11 @@
// router.go
package auth
import (
"api_yal/internal/logger"
"api_yal/internal/middleware"
"api_yal/internal/repository"
"time"
"github.com/go-chi/chi/v5"
"gorm.io/gorm"
@@ -13,8 +15,16 @@ import (
func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) {
// Создаем репозиторий и сервис
accountRepo := repository.NewAccountRepository(db)
authService := NewAuthService(accountRepo, jwtSecret)
handler := NewAuthHandler(&authService)
// Конфигурация токенов
authConfig := AuthServiceConfig{
JWTSecret: jwtSecret,
AccessTokenTTL: 15 * time.Minute, // Access token живет 15 минут
RefreshTokenTTL: 7 * 24 * time.Hour, // Refresh token живет 7 дней
}
authService := NewAuthService(accountRepo, authConfig)
handler := NewAuthHandler(authService)
l := logger.Get()
l.Debug("Регистрация маршрутов аутентификации")
@@ -1,3 +1,4 @@
// service.go
package auth
import (
@@ -16,25 +17,73 @@ import (
// AuthService интерфейс сервиса аутентификации
type AuthService interface {
Register(req RegisterRequest) (*AuthResponse, error)
Login(req LoginRequest) (*AuthResponse, error)
RefreshToken(refreshToken string) (*AuthResponse, error)
Login(req LoginRequest) (*AuthResponse, string, error) // Возвращает refresh token отдельно
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)
}
// authServiceImpl реализация сервиса аутентификации
type authServiceImpl struct {
accountRepo repository.AccountRepository
jwtSecret []byte
accessTokenTTL time.Duration
refreshTokenTTL time.Duration
}
// AuthServiceConfig конфигурация для сервиса аутентификации
type AuthServiceConfig struct {
JWTSecret string
AccessTokenTTL time.Duration // Рекомендуется 15-30 минут
RefreshTokenTTL time.Duration // Рекомендуется 7-30 дней
}
// NewAuthService создает новый экземпляр сервиса аутентификации
func NewAuthService(accountRepo repository.AccountRepository, jwtSecret string) AuthService {
func NewAuthService(accountRepo repository.AccountRepository, config AuthServiceConfig) AuthService {
return &authServiceImpl{
accountRepo: accountRepo,
jwtSecret: []byte(jwtSecret),
jwtSecret: []byte(config.JWTSecret),
accessTokenTTL: config.AccessTokenTTL,
refreshTokenTTL: config.RefreshTokenTTL,
}
}
// 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()
@@ -74,8 +123,8 @@ func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) {
return nil, err
}
// Генерируем JWT токены
accessToken, refreshToken, expiresAt, err := s.generateTokens(newAcc)
// Генерируем access token
accessToken, expiresAt, err := s.generateAccessToken(newAcc)
if err != nil {
return nil, err
}
@@ -83,7 +132,6 @@ func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) {
// Формируем ответ
response := &AuthResponse{
Token: accessToken,
RefreshToken: refreshToken,
ExpiresAt: expiresAt,
User: UserInfo{
ID: newAcc.Base.ID,
@@ -99,8 +147,8 @@ func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) {
return response, nil
}
// Login аутентифицирует пользователя
func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, error) {
// Login аутентифицирует пользователя и возвращает access и refresh токены
func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, string, error) {
l := logger.Get()
l.Info("Начало входа пользователя", zap.String("email", req.Email))
@@ -108,31 +156,35 @@ func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, error) {
account, err := s.accountRepo.GetByEmail(req.Email)
if err != nil {
l.Error("Пользователь не найден", zap.String("email", req.Email), zap.Error(err))
return nil, ErrUserNotFound
return nil, "", ErrUserNotFound
}
// Проверяем, активен ли аккаунт
if !account.IsActive {
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 {
l.Error("Неверный пароль для пользователя", zap.String("email", req.Email))
return nil, ErrInvalidPassword
return nil, "", ErrInvalidPassword
}
// Генерируем JWT токены
accessToken, refreshToken, expiresAt, err := s.generateTokens(account)
// Генерируем токены
accessToken, expiresAt, err := s.generateAccessToken(account)
if err != nil {
return nil, err
return nil, "", err
}
refreshToken, err := s.generateRefreshToken(account)
if err != nil {
return nil, "", err
}
// Формируем ответ
response := &AuthResponse{
Token: accessToken,
RefreshToken: refreshToken,
ExpiresAt: expiresAt,
User: UserInfo{
ID: account.Base.ID,
@@ -145,53 +197,33 @@ func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, error) {
}
l.Info("Пользователь успешно вошел", zap.String("email", req.Email))
return response, nil
return response, refreshToken, nil
}
// RefreshToken обновляет JWT токен по refresh token
func (s *authServiceImpl) RefreshToken(refreshToken string) (*AuthResponse, error) {
func (s *authServiceImpl) RefreshToken(refreshToken string) (*RefreshTokenResponse, error) {
l := logger.Get()
l.Info("Начало обновления токена")
// Парсим и валидируем 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 || !token.Valid {
l.Error("Невалидный refresh token", zap.Error(err))
return nil, errors.New("invalid refresh token")
}
// Получаем claims
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
l.Error("Не удалось получить claims из токена")
return nil, errors.New("invalid token claims")
}
// Проверяем тип токена (должен быть refresh)
if tokenType, exists := claims["type"]; !exists || tokenType != "refresh" {
l.Error("Токен не является refresh токеном")
return nil, errors.New("invalid token type")
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)
userIDStr, ok := (*claims)["sub"].(string)
if !ok {
l.Error("Не удалось получить subject из токена")
return nil, errors.New("invalid token claims")
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, errors.New("invalid user id in token")
return nil, ErrInvalidToken
}
// Получаем пользователя из базы по ID
@@ -207,17 +239,16 @@ func (s *authServiceImpl) RefreshToken(refreshToken string) (*AuthResponse, erro
return nil, errors.New("account is deactivated")
}
// Генерируем новую пару токенов
accessToken, newRefreshToken, expiresAt, err := s.generateTokens(account)
// Генерируем новый access token
accessToken, expiresAt, err := s.generateAccessToken(account)
if err != nil {
l.Error("Ошибка генерации токенов", zap.Error(err))
l.Error("Ошибка генерации access token", zap.Error(err))
return nil, err
}
l.Info("Обновление токена успешно завершено", zap.Uint("userID", userID))
return &AuthResponse{
return &RefreshTokenResponse{
Token: accessToken,
RefreshToken: newRefreshToken,
ExpiresAt: expiresAt,
User: UserInfo{
ID: account.Base.ID,
@@ -235,48 +266,132 @@ 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
}
// 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)
// 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
})
// Refresh token expires in 7 days
refreshExpiresAt := time.Now().Add(7 * 24 * time.Hour)
if err != nil {
return nil, err
}
// Создаем access token
accessClaims := jwt.MapClaims{
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": accessExpiresAt.Unix(),
"exp": expiresAt.Unix(),
"iat": time.Now().Unix(),
"type": "access",
}
accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessToken, err = accessTokenObj.SignedString(s.jwtSecret)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(s.jwtSecret)
if err != nil {
return "", "", time.Time{}, err
return "", time.Time{}, err
}
// Создаем refresh token
refreshClaims := jwt.MapClaims{
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": refreshExpiresAt.Unix(),
"exp": expiresAt.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)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(s.jwtSecret)
if err != nil {
return "", "", time.Time{}, err
return "", err
}
return accessToken, refreshToken, accessExpiresAt, nil
return tokenString, 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
}
@@ -1,7 +1,9 @@
// middleware/auth.go (обновленная версия с логированием)
package middleware
import (
"context"
"fmt"
"net/http"
"strings"
@@ -14,11 +16,8 @@ import (
type contextKey string
const (
// UserIDKey ключ для хранения ID пользователя в контексте
UserIDKey contextKey = "userID"
// UserEmailKey ключ для хранения email пользователя в контексте
UserEmailKey contextKey = "userEmail"
// UserRoleKey ключ для хранения роли пользователя в контексте
UserRoleKey contextKey = "userRole"
)
@@ -28,10 +27,15 @@ func AuthMiddleware(jwtSecret string) func(http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
l := logger.Get()
l.Info("=== AUTH MIDDLEWARE START ===")
l.Info("Request path", zap.String("path", r.URL.Path))
// Получаем токен из заголовка Authorization
authHeader := r.Header.Get("Authorization")
l.Info("Authorization header", zap.String("header", authHeader))
if authHeader == "" {
l.Debug("Отсутствует заголовок Authorization")
l.Warn("Отсутствует заголовок Authorization")
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
@@ -39,63 +43,101 @@ func AuthMiddleware(jwtSecret string) func(http.Handler) http.Handler {
// Ожидаем формат "Bearer <token>"
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
l.Debug("Неверный формат заголовка Authorization")
l.Warn("Неверный формат заголовка Authorization",
zap.Int("parts_count", len(parts)),
zap.String("first_part", parts[0]))
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
return
}
tokenString := parts[1]
l.Info("Token extracted", zap.String("token_preview", tokenString[:min(20, len(tokenString))]+"..."))
// Парсим и валидируем токен
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Проверяем метод подписи
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
l.Error("Unexpected signing method",
zap.String("method", token.Method.Alg()))
return nil, jwt.ErrSignatureInvalid
}
return []byte(jwtSecret), nil
})
if err != nil || !token.Valid {
l.Debug("Невалидный токен: %v", zap.Error(err))
if err != nil {
l.Error("Token parse error", zap.Error(err))
http.Error(w, "Invalid token: "+err.Error(), http.StatusUnauthorized)
return
}
if !token.Valid {
l.Error("Token is not valid")
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
l.Info("Token is valid")
// Извлекаем claims
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
l.Debug("Не удалось извлечь claims из токена")
l.Error("Failed to extract claims")
http.Error(w, "Invalid token claims", http.StatusUnauthorized)
return
}
l.Info("Claims extracted", zap.Any("claims", claims))
// Проверяем тип токена (должен быть access)
if tokenType, exists := claims["type"]; exists {
l.Info("Token type", zap.String("type", tokenType.(string)))
if tokenType != "access" {
l.Error("Wrong token type, expected access", zap.String("type", tokenType.(string)))
http.Error(w, "Invalid token type", http.StatusUnauthorized)
return
}
}
// Добавляем информацию о пользователе в контекст
ctx := r.Context()
// Извлекаем userID из sub (subject)
if userID, ok := claims["sub"].(float64); ok {
ctx = context.WithValue(ctx, UserIDKey, uint(userID))
// В claims sub хранится как string, а не float64
if userID, ok := claims["sub"].(string); ok {
l.Info("User ID from claims", zap.String("user_id_str", userID))
// Конвертируем string в uint
var userIDUint uint
if _, err := fmt.Sscan(userID, &userIDUint); err == nil {
ctx = context.WithValue(ctx, UserIDKey, userIDUint)
l.Info("User ID added to context", zap.Uint("user_id", userIDUint))
}
} else {
l.Error("sub claim not found or wrong type")
}
// Извлекаем email
if email, ok := claims["email"].(string); ok {
ctx = context.WithValue(ctx, UserEmailKey, email)
l.Info("Email added to context", zap.String("email", email))
}
// Извлекаем роль
if role, ok := claims["role"].(string); ok {
ctx = context.WithValue(ctx, UserRoleKey, role)
l.Info("Role added to context", zap.String("role", role))
}
l.Info("=== AUTH MIDDLEWARE END ===")
// Передаем управление дальше с обновленным контекстом
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// AuthMiddlewareWithContext (для обратной совместимости)
func AuthMiddlewareWithContext(next http.Handler) http.Handler {
// Эта функция должна быть реализована в основном приложении
// с передачей jwtSecret из конфигурации
return next
func min(a, b int) int {
if a < b {
return a
}
return b
}
@@ -4,7 +4,6 @@ import (
"api_yal/internal/config"
"api_yal/internal/logger"
"api_yal/internal/domain/auth"
CastomMiddleware "api_yal/internal/middleware"
"time"
"encoding/json"
@@ -29,7 +28,6 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
addProductionMiddleware(r, config)
// 2. Затем добавляем middleware аутентификации (он тоже применяется ко всем маршрутам)
r.Use(CastomMiddleware.AuthMiddlewareWithContext)
zapLogger.Debug("Auth middleware применён")
// 3. И только потом регистрируем маршруты