Невозможно заставить маршрут перенаправлять на страницу входа в систему, если пользователь не аутентифицирован в приложении React/Django

Я использую React для фронтенда и Django для бэкенда, оба варианта являются новыми для меня, и я потратил кучу времени на эту проблему, не решив ее, поэтому я очень надеюсь, что кто-то знает ответ.

Когда я захожу на свой сайт http://localhost:4000/, не будучи авторизованным, я вижу черный экран с текстом "Loading..." (см. код protected_route ниже), вместо того чтобы быть перенаправленным на страницу входа в систему. Это происходит, когда я перехожу к любым защищенным маршрутам, не войдя в систему. В настоящее время единственными маршрутами, которые не защищены, являются страницы входа и регистрации, которые, если я вручную перехожу на них, работают правильно. После входа в систему токены JWT корректно сохраняются в куки httpOnly, но я вижу страницу "Loading...", пока не обновлю страницу вручную, тогда я вижу свои защищенные маршруты.

Я подумал, может быть, проверка подлинности происходит слишком быстро, и состояние еще не готово, и оно застревает на экране "Loading..."? Но я не знаю, как это исправить, если это так...

Я опубликую соответствующий код ниже, но я также поделюсь репозиторием github здесь.

Вот консоль браузера/сервера при переходе на страницу без авторизации:

GET http://localhost:8000/accounts/auth/user/ 401 (Unauthorized)          AuthContext.tsx:37
GET http://localhost:8000/accounts/auth/user/ 401 (Unauthorized)          AuthContext.tsx:37
POST http://localhost:8000/accounts/auth/token/refresh/ 401 (Unauthorized)          axiosInstance.tsx:74
WARNING:django.request:Unauthorized: /accounts/auth/user/
[20/Jun/2024 01:47:58] "GET /accounts/auth/user/ HTTP/1.1" 401 58
Unauthorized: /accounts/auth/user/
WARNING:django.request:Unauthorized: /accounts/auth/user/
[20/Jun/2024 01:47:58] "GET /accounts/auth/user/ HTTP/1.1" 401 58
Unauthorized: /accounts/auth/token/refresh/
WARNING:django.request:Unauthorized: /accounts/auth/token/refresh/
[20/Jun/2024 01:47:58] "POST /accounts/auth/token/refresh/ HTTP/1.1" 401 67

AuthContext.tsx

import { createContext, useContext, useState, useEffect, ReactNode, FC } from 'react';
import axiosInstance from '../axiosInstance';

interface AuthContextType {
    isAuthenticated: boolean;
    loading: boolean;
    login: (username: string, password: string, navigate: () => void) => Promise<void>;
    logout: () => void;
    register: (username: string, email: string, password: string, password2: string) => Promise<void>;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export const AuthProvider: FC<{ children: ReactNode }> = ({ children }) => {
    const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
    const [loading, setLoading] = useState<boolean>(true);

    useEffect(() => {
        const checkAuthStatus = async () => {
            try {
                await axiosInstance.get('/auth/user/');
                setIsAuthenticated(true);
            } catch (error) {
                if (error.response && error.response.status === 401) {
                    try {
                        await axiosInstance.post('/auth/token/refresh/');
                        await axiosInstance.get('/auth/user/');
                        setIsAuthenticated(true);
                    } catch (refreshError) {
                        setIsAuthenticated(false);
                    }
                } else {
                    setIsAuthenticated(false);
                }
            } finally {
                setLoading(false);
            }
        };
        checkAuthStatus();
    }, []);

    // ... (login, logout, register functions)

    return (
        <AuthContext.Provider value={{ isAuthenticated, loading, login, logout, register }}>
            {children}
        </AuthContext.Provider>
    );
};

export const useAuth = () => {
    const context = useContext(AuthContext);
    if (!context) {
        throw new Error('useAuth must be used within an AuthProvider');
    }
    return context;
};

axiosInstance.tsx

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import Cookies from 'js-cookie'

const axiosInstance: AxiosInstance = axios.create({
    baseURL: 'http://localhost:8000/accounts/',
    timeout: 5000,
    withCredentials: true,
})

axiosInstance.interceptors.request.use(
    (config) => {
        const csrfToken = Cookies.get('csrftoken')
        if (csrfToken) {
            config.headers['X-CSRFToken'] = csrfToken
        }
        return config
    },
    (error) => Promise.reject(error)
)

let isRefreshing = false
let failedQueue: any[] = []

const processQueue = (error: any, token: string | null = null) => {
    failedQueue.forEach((prom: any) => {
        if (error) {
            prom.reject(error)
        } else {
            prom.resolve(token)
        }
    })
    failedQueue = []
}

axiosInstance.interceptors.response.use(
    (response: AxiosResponse) => response,
    async (error) => {
        const originalRequest: AxiosRequestConfig & { _retry?: boolean, _retryCount?: number } = error.config

        if (error.response && error.response.status === 401 && !originalRequest._retry) {
            if (isRefreshing) {
                return new Promise((resolve, reject) => {
                    failedQueue.push({ resolve, reject })
                })
                .then((token: string) => {
                    originalRequest.headers['Authorization'] = 'Bearer ' + token
                    return axiosInstance(originalRequest)
                })
                .catch((err) => Promise.reject(err))
            }

            originalRequest._retry = true
            originalRequest._retryCount = (originalRequest._retryCount || 0) + 1

            if (originalRequest._retryCount > 3) {
                return Promise.reject(error)
            }

            isRefreshing = true

            return new Promise((resolve, reject) => {
                axiosInstance.post('/auth/token/refresh/')
                    .then(({ data }) => {
                        axiosInstance.defaults.headers['Authorization'] = 'Bearer ' + data.access
                        originalRequest.headers['Authorization'] = 'Bearer ' + data.access
                        processQueue(null, data.access)
                        resolve(axiosInstance(originalRequest))
                    })
                    .catch((err) => {
                        processQueue(err, null)
                        reject(err)
                    })
                    .finally(() => {
                        isRefreshing = false
                    })
            })
        }
        return Promise.reject(error)
    }
)
export default axiosInstance

protected_route.tsx:

import { Navigate, Outlet } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'

const ProtectedRoute = () => {
    const { isAuthenticated, loading } = useAuth()

    if (loading) {
        return <div>Loading...</div> // Display a loading indicator while checking auth status
    }

    return isAuthenticated ? <Outlet /> : <Navigate to="/login" />
}

export default ProtectedRoute

router.tsx:

import { Routes, Route, BrowserRouter } from 'react-router-dom'
import ProtectedRoute from '../components/protected_route'
import Dashboard from '../pages/dashboard'
import Login from '../pages/login'
import Register from '../pages/register'
import TestPage from '../pages/testPage'
import MainLayout from '../layouts/main_layout'
import Trades from '../pages/trades'
import LoginSignUpLayout from '../layouts/login_register_layout'

const AppRoutes = () => (
    <BrowserRouter>
        <Routes>
            <Route path="/" element={<LoginSignUpLayout />}>
                <Route path="/login" element={<Login />} />
                <Route path="/register" element={<Register />} />
            </Route>
            <Route element={<ProtectedRoute />}>
                <Route element={<MainLayout />}>
                    <Route path="/" index element={<Dashboard />} />
                    <Route path="trades" element={<Trades />} />
                    <Route path="TestPage" element={<TestPage />} />
                    {/* Add more protected routes here */}
                </Route>
            </Route>
        </Routes>
    </BrowserRouter>
)

export default AppRoutes

Backend: cookie_jwt_middleware.py:

from django.utils.deprecation import MiddlewareMixin

class CookieJWTMiddleware(MiddlewareMixin):
    def process_request(self, request):
        access_token = request.COOKIES.get('access_token')
        if access_token:
            request.META['HTTP_AUTHORIZATION'] = f'Bearer {access_token}'

accounts/urls.py:

from django.urls import path, include
from .views import CustomTokenObtainPairView, CustomTokenRefreshView, LogoutView, ProfileView, CustomLoginView

urlpatterns = [
    path('auth/', include('dj_rest_auth.urls')),
    path('auth/registration/', include('dj_rest_auth.registration.urls')),
    path('auth/login/', CustomLoginView.as_view(), name='rest_login'),  # Custom login view
    path('auth/token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),  # Custom token obtain view
    path('auth/token/refresh/', CustomTokenRefreshView.as_view(), name='token_refresh'),  # Custom token refresh view
    path('auth/logout/', LogoutView.as_view(), name='rest_logout'),  # Custom logout view
    path('profile/', ProfileView.as_view(), name='profile'),  # Profile view
]

accounts/views.py:

from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from django.http import JsonResponse
from dj_rest_auth.views import LoginView as DjRestAuthLoginView
from rest_framework import status
from django.utils.decorators import method_decorator
from django.views.decorators.debug import sensitive_post_parameters

class CustomTokenObtainPairView(TokenObtainPairView):
    def post(self, request, *args, **kwargs):
        response = super().post(request, *args, **kwargs)
        if response.status_code == 200:
            tokens = response.data
            access_token = tokens['access']
            refresh_token = tokens['refresh']

            response = JsonResponse({'detail': 'Login successful'})
            response.set_cookie(
                key='access_token',
                value=access_token,
                httponly=True,
                secure=True, 
                samesite='Lax'
            )
            response.set_cookie(
                key='refresh_token',
                value=refresh_token,
                httponly=True,
                secure=True, 
                samesite='Lax'
            )
        return response

class CustomTokenRefreshView(TokenRefreshView):
    def post(self, request, *args, **kwargs):
        refresh_token = request.COOKIES.get('refresh_token')
        if not refresh_token:
            return JsonResponse({'detail': 'Refresh token not provided'}, status=400)

        serializer = self.get_serializer(data={'refresh': refresh_token})
        try:
            serializer.is_valid(raise_exception=True)
        except TokenError as e:
            raise InvalidToken(e.args[0])

        access_token = serializer.validated_data['access']
        response = JsonResponse({'detail': 'Token refreshed'})
        response.set_cookie(
            key='access_token',
            value=access_token,
            httponly=True,
            secure=True, 
            samesite='Lax'
        )
        return response

class CustomLoginView(DjRestAuthLoginView):
    @method_decorator(sensitive_post_parameters('password'))
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)

    def post(self, request, *args, **kwargs):
        response = super().post(request, *args, **kwargs)
        
        if response.status_code == 200:
            # If login is successful, return a JSON response
            return JsonResponse({
                'detail': 'Login successful',
                'user': request.user.username
            }, status=status.HTTP_200_OK)
        else:
            # If login fails, return the response as is
            return response
Вернуться на верх