package middleware import ( "context" "fmt" "net/http" "strings" "api_yal/internal/logger" "github.com/golang-jwt/jwt/v5" "go.uber.org/zap" ) type contextKey string const ( UserIDKey contextKey = "userID" UserEmailKey contextKey = "userEmail" UserRoleKey contextKey = "userRole" ) // AuthMiddleware создает middleware для проверки JWT токена func AuthMiddleware(jwtSecret string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := logger.Get() // Логируем только в debug режиме, чтобы не засорять логи l.Debug("Auth middleware: processing request", zap.String("path", r.URL.Path), zap.String("method", r.Method)) // Получаем токен из заголовка Authorization authHeader := r.Header.Get("Authorization") if authHeader == "" { l.Debug("Authorization header missing") http.Error(w, "Authorization header required", http.StatusUnauthorized) return } // Ожидаем формат "Bearer " parts := strings.Split(authHeader, " ") if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { l.Debug("Invalid authorization header format", zap.String("header", authHeader)) http.Error(w, "Invalid authorization header format", http.StatusUnauthorized) return } tokenString := parts[1] // Парсим и валидируем токен claims, err := validateToken(tokenString, jwtSecret) if err != nil { l.Debug("Token validation failed", zap.Error(err)) // Возвращаем разные сообщения в зависимости от ошибки if err == jwt.ErrTokenExpired { http.Error(w, "Token expired", http.StatusUnauthorized) return } http.Error(w, "Invalid token", http.StatusUnauthorized) return } // Добавляем информацию о пользователе в контекст ctx := addUserToContext(r.Context(), claims) // Логируем успешную аутентификацию (только в debug) if userID, ok := ctx.Value(UserIDKey).(uint); ok { l.Debug("User authenticated", zap.Uint("user_id", userID), zap.String("path", r.URL.Path)) } // Передаем управление дальше с обновленным контекстом next.ServeHTTP(w, r.WithContext(ctx)) }) } } // validateToken валидирует JWT токен и возвращает claims func validateToken(tokenString, jwtSecret string) (jwt.MapClaims, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { // Проверяем метод подписи if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(jwtSecret), nil }) if err != nil { return nil, err } if !token.Valid { return nil, fmt.Errorf("invalid token") } claims, ok := token.Claims.(jwt.MapClaims) if !ok { return nil, fmt.Errorf("invalid claims") } // Проверяем тип токена (должен быть access) if tokenType, exists := claims["type"]; !exists || tokenType != "access" { return nil, fmt.Errorf("invalid token type, expected access") } return claims, nil } // addUserToContext добавляет информацию о пользователе в контекст func addUserToContext(ctx context.Context, claims jwt.MapClaims) context.Context { // Извлекаем userID из sub (subject) if userIDStr, ok := claims["sub"].(string); ok { var userID uint if _, err := fmt.Sscan(userIDStr, &userID); err == nil { ctx = context.WithValue(ctx, UserIDKey, userID) } } // Извлекаем email if email, ok := claims["email"].(string); ok { ctx = context.WithValue(ctx, UserEmailKey, email) } // Извлекаем роль if role, ok := claims["role"].(string); ok { ctx = context.WithValue(ctx, UserRoleKey, role) } return ctx } // GetUserID извлекает ID пользователя из контекста func GetUserID(ctx context.Context) (uint, bool) { userID, ok := ctx.Value(UserIDKey).(uint) return userID, ok } // GetUserEmail извлекает email пользователя из контекста func GetUserEmail(ctx context.Context) (string, bool) { email, ok := ctx.Value(UserEmailKey).(string) return email, ok } // GetUserRole извлекает роль пользователя из контекста func GetUserRole(ctx context.Context) (string, bool) { role, ok := ctx.Value(UserRoleKey).(string) return role, ok }