diff --git a/backend/authentications/urls.py b/backend/authentications/urls.py index 32965a4..75c5914 100644 --- a/backend/authentications/urls.py +++ b/backend/authentications/urls.py @@ -1,6 +1,6 @@ from django.urls import path from rest_framework_simplejwt import views as jwt_views -from authentications.views import ObtainTokenPairWithCustomView, GreetingView, GoogleLogin, GoogleRetrieveUserInfo +from authentications.views import ObtainTokenPairWithCustomView, GreetingView, GoogleLogin, GoogleRetrieveUserInfo, CheckAccessTokenAndRefreshToken urlpatterns = [ path('token/obtain/', jwt_views.TokenObtainPairView.as_view(), name='token_create'), @@ -9,4 +9,5 @@ urlpatterns = [ path('hello/', GreetingView.as_view(), name='hello_world'), path('dj-rest-auth/google/', GoogleLogin.as_view(), name="google_login"), path('auth/google/', GoogleRetrieveUserInfo.as_view()), + path('auth/status/', CheckAccessTokenAndRefreshToken.as_view(), name='check_token_status') ] \ No newline at end of file diff --git a/backend/authentications/views.py b/backend/authentications/views.py index 9b5f249..167581b 100644 --- a/backend/authentications/views.py +++ b/backend/authentications/views.py @@ -1,6 +1,3 @@ -from django.shortcuts import render - -# Create your views here. """This module defines API views for authentication, user creation, and a simple hello message.""" import json @@ -14,6 +11,8 @@ from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.response import Response from rest_framework.views import APIView from rest_framework_simplejwt.tokens import RefreshToken +from rest_framework_simplejwt.authentication import JWTAuthentication + from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter @@ -27,6 +26,31 @@ from users.managers import CustomAccountManager from users.models import CustomUser +class CheckAccessTokenAndRefreshToken(APIView): + permission_classes = (AllowAny,) + JWT_authenticator = JWTAuthentication() + + def post(self, request, *args, **kwargs): + access_token = request.data.get('access_token') + refresh_token = request.data.get('refresh_token') + # Check if the access token is valid + if access_token: + response = self.JWT_authenticator.authenticate(request) + if response is not None: + return Response({'status': 'true'}, status=status.HTTP_200_OK) + + # Check if the refresh token is valid + if refresh_token: + try: + refresh = RefreshToken(refresh_token) + access_token = str(refresh.access_token) + return Response({'access_token': access_token}, status=status.HTTP_200_OK) + except Exception as e: + return Response({'status': 'false'}, status=status.HTTP_401_UNAUTHORIZED) + + return Response({'status': 'false'}, status=status.HTTP_400_BAD_REQUEST) + + class ObtainTokenPairWithCustomView(APIView): """ Custom Token Obtain Pair View. @@ -165,4 +189,4 @@ class GoogleRetrieveUserInfo(APIView): response = requests.get(api_url, headers=headers) if response.status_code == 200: return response.json() - raise Exception('Google API Error', response) + raise Exception('Google API Error', response) \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 714e1c7..70698dc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,6 +37,7 @@ "framer-motion": "^10.16.4", "gapi-script": "^1.2.0", "jwt-decode": "^4.0.0", + "prop-types": "^15.8.1", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^2.9.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 26c8422..143df2b 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -86,6 +86,9 @@ dependencies: jwt-decode: specifier: ^4.0.0 version: 4.0.0 + prop-types: + specifier: ^15.8.1 + version: 15.8.1 react: specifier: ^18.2.0 version: 18.2.0 @@ -3380,14 +3383,6 @@ packages: warning: 4.0.3 dev: false - /react-day-picker@8.9.1(date-fns@2.30.0)(react@18.2.0): - resolution: {integrity: sha512-W0SPApKIsYq+XCtfGeMYDoU0KbsG3wfkYtlw8l+vZp6KoBXGOlhzBUp4tNx1XiwiOZwhfdGOlj7NGSCKGSlg5Q==} - peerDependencies: - date-fns: ^2.28.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - date-fns: 2.30.0 - react: 18.2.0 /react-calendar@4.6.1(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-MvCPdvxEvq7wICBhFxlYwxS2+IsVvSjTcmlr0Kl3yDRVhoX7btNg0ySJx5hy9rb1eaM4nDpzQcW5c87nfQ8n8w==} peerDependencies: @@ -3479,6 +3474,16 @@ packages: - '@types/react-dom' dev: false + /react-day-picker@8.9.1(date-fns@2.30.0)(react@18.2.0): + resolution: {integrity: sha512-W0SPApKIsYq+XCtfGeMYDoU0KbsG3wfkYtlw8l+vZp6KoBXGOlhzBUp4tNx1XiwiOZwhfdGOlj7NGSCKGSlg5Q==} + peerDependencies: + date-fns: ^2.28.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + date-fns: 2.30.0 + react: 18.2.0 + dev: false + /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -3511,8 +3516,6 @@ packages: tiny-warning: 1.0.3 dev: false - /react-icons@4.11.0(react@18.2.0): - resolution: {integrity: sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA==} /react-icons@4.12.0(react@18.2.0): resolution: {integrity: sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==} peerDependencies: @@ -3611,18 +3614,6 @@ packages: react-transition-group: 2.9.0(react-dom@18.2.0)(react@18.2.0) dev: false - /react-transition-group@2.9.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==} - peerDependencies: - react: '>=15.0.0' - react-dom: '>=15.0.0' - dependencies: - dom-helpers: 3.4.0 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-lifecycles-compat: 3.0.4 /react-time-picker@6.5.2(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-xRamxjndpq3HfnEL+6T3VyirLMEn4D974OJgs9sTP8iJX/RB02rmwy09C9oBThTGuN3ycbozn06iYLn148vcdw==} peerDependencies: @@ -3648,6 +3639,20 @@ packages: - '@types/react-dom' dev: false + /react-transition-group@2.9.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==} + peerDependencies: + react: '>=15.0.0' + react-dom: '>=15.0.0' + dependencies: + dom-helpers: 3.4.0 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-lifecycles-compat: 3.0.4 + dev: false + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b6d42b2..83ebef6 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,7 @@ +import { useEffect } from "react"; import "./App.css"; -import { Route, Routes, useLocation } from "react-router-dom"; - +import { Route, Routes } from "react-router-dom"; +import axios from "axios"; import TestAuth from "./components/testAuth"; import LoginPage from "./components/authentication/LoginPage"; import SignUpPage from "./components/authentication/SignUpPage"; @@ -13,18 +14,66 @@ import PrivateRoute from "./PrivateRoute"; import ProfileUpdatePage from "./components/profilePage"; import Dashboard from "./components/dashboard/dashboard"; +import { useAuth } from "./hooks/AuthHooks"; const App = () => { - const location = useLocation(); - const prevention = ["/login", "/signup"]; - const isLoginPageOrSignUpPage = prevention.some(_ => location.pathname.includes(_)); + const { isAuthenticated, setIsAuthenticated } = useAuth(); + useEffect(() => { + const checkLoginStatus = async () => { + const data = { + access_token: localStorage.getItem("access_token"), + refresh_token: localStorage.getItem("refresh_token"), + }; + + await axios + .post("http://127.0.0.1:8000/api/auth/status/", data, { + headers: { + Authorization: "Bearer " + localStorage.getItem("access_token"), + }, + }) + .then((response) => { + if (response.status === 200) { + if (response.data.access_token) { + localStorage.setItem("access_token", response.data.access_token); + setIsAuthenticated(true); + } else { + setIsAuthenticated(true); + } + } else { + setIsAuthenticated(false); + } + }) + .catch((error) => { + console.error("Error checking login status:", error.message); + }); + }; + + checkLoginStatus(); + }, [setIsAuthenticated]); + + return
{isAuthenticated ? : }
; +}; + +const NonAuthenticatedComponents = () => { return ( -
- {!isLoginPageOrSignUpPage && } -
+
+ + + } /> + } /> + +
+ ); +}; + +const AuthenticatedComponents = () => { + return ( +
+ +
-
+
} /> }> diff --git a/frontend/src/PrivateRoute.jsx b/frontend/src/PrivateRoute.jsx index defeaea..b9784f6 100644 --- a/frontend/src/PrivateRoute.jsx +++ b/frontend/src/PrivateRoute.jsx @@ -1,11 +1,9 @@ -import React from "react"; import { Navigate, Outlet } from "react-router-dom"; -import { useAuth } from "./hooks/authentication/IsAuthenticated"; +import { useAuth } from "src/hooks/AuthHooks"; const PrivateRoute = () => { - const { isAuthenticated, setIsAuthenticated } = useAuth(); - const auth = isAuthenticated; - return auth ? : ; + const { isAuthenticated } = useAuth(); + return isAuthenticated ? : ; }; -export default PrivateRoute; \ No newline at end of file +export default PrivateRoute; diff --git a/frontend/src/api/AuthenticationApi.jsx b/frontend/src/api/AuthenticationApi.jsx index 1913922..7f88744 100644 --- a/frontend/src/api/AuthenticationApi.jsx +++ b/frontend/src/api/AuthenticationApi.jsx @@ -7,6 +7,7 @@ const apiUserLogin = (data) => { .post("token/obtain/", data) .then((response) => { console.log(response.statusText); + return response; }) .catch((error) => { diff --git a/frontend/src/api/AxiosConfig.jsx b/frontend/src/api/AxiosConfig.jsx index 336d18d..7b701bd 100644 --- a/frontend/src/api/AxiosConfig.jsx +++ b/frontend/src/api/AxiosConfig.jsx @@ -1,4 +1,5 @@ import axios from "axios"; +import { redirect } from "react-router-dom"; const axiosInstance = axios.create({ baseURL: "http://127.0.0.1:8000/api/", @@ -18,24 +19,24 @@ axiosInstance.interceptors.response.use( const refresh_token = localStorage.getItem("refresh_token"); // Check if the error is due to 401 and a refresh token is available - if ( - error.response.status === 401 && - error.response.statusText === "Unauthorized" && - refresh_token !== "undefined" - ) { - return axiosInstance - .post("/token/refresh/", { refresh: refresh_token }) - .then((response) => { - localStorage.setItem("access_token", response.data.access); + if (error.response && error.response.status === 401) { + if (refresh_token) { + return axiosInstance + .post("/token/refresh/", { refresh: refresh_token }) + .then((response) => { + localStorage.setItem("access_token", response.data.access); - axiosInstance.defaults.headers["Authorization"] = "Bearer " + response.data.access; - originalRequest.headers["Authorization"] = "Bearer " + response.data.access; + axiosInstance.defaults.headers["Authorization"] = "Bearer " + response.data.access; + originalRequest.headers["Authorization"] = "Bearer " + response.data.access; - return axiosInstance(originalRequest); - }) - .catch((err) => { - console.log("Interceptors error: ", err); - }); + return axiosInstance(originalRequest); + }) + .catch((err) => { + console.log("Interceptors error: ", err); + }); + } else { + redirect("/login"); + } } return Promise.reject(error); } diff --git a/frontend/src/contexts/AuthContextProvider.jsx b/frontend/src/contexts/AuthContextProvider.jsx new file mode 100644 index 0000000..fea0243 --- /dev/null +++ b/frontend/src/contexts/AuthContextProvider.jsx @@ -0,0 +1,29 @@ +import { useEffect } from "react"; +import PropTypes from "prop-types"; +import { createContext, useState } from "react"; + +const AuthContext = createContext(); + +export const AuthProvider = ({ children }) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + + useEffect(() => { + const accessToken = localStorage.getItem("access_token"); + if (accessToken) { + setIsAuthenticated(true); + } + }, []); + + const contextValue = { + isAuthenticated, + setIsAuthenticated, + }; + + return {children}; +}; + +AuthProvider.propTypes = { + children: PropTypes.node.isRequired, +}; + +export default AuthContext; diff --git a/frontend/src/hooks/AuthHooks.jsx b/frontend/src/hooks/AuthHooks.jsx new file mode 100644 index 0000000..27d3afb --- /dev/null +++ b/frontend/src/hooks/AuthHooks.jsx @@ -0,0 +1,36 @@ +import { useContext } from "react"; +import AuthContext from "src/contexts/AuthContextProvider"; + +/** + * useAuth - Custom React Hook for Accessing Authentication Context + * + * @returns {Object} An object containing: + * - {boolean} isAuthenticated: A boolean indicating whether the user is authenticated. + * - {function} setIsAuthenticated: A function to set the authentication status manually. + * + * @throws {Error} If used outside the context of an AuthProvider. + * + * @example + * // Import the hook + * import useAuth from './AuthHooks'; + * + * // Inside a functional component + * const { isAuthenticated, setIsAuthenticated } = useAuth(); + * + * // Check authentication status + * if (isAuthenticated) { + * // User is authenticated + * } else { + * // User is not authenticated + * } + * + * // Manually set authentication status + * setIsAuthenticated(true); + */ +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +}; diff --git a/frontend/src/hooks/authentication/IsAuthenticated.jsx b/frontend/src/hooks/authentication/IsAuthenticated.jsx deleted file mode 100644 index 8874645..0000000 --- a/frontend/src/hooks/authentication/IsAuthenticated.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { createContext, useContext, useState, useEffect } from "react"; - -const AuthContext = createContext(); - -export const AuthProvider = ({ children }) => { - const [isAuthenticated, setIsAuthenticated] = useState(() => { - const access_token = localStorage.getItem("access_token"); - return !!access_token; - }); - - useEffect(() => { - const handleTokenChange = () => { - const newAccessToken = localStorage.getItem("access_token"); - setIsAuthenticated(!!newAccessToken); - }; - - handleTokenChange(); - - window.addEventListener("storage", handleTokenChange); - - return () => { - window.removeEventListener("storage", handleTokenChange); - }; - }, []); - - return ( - - {children} - - ); -}; - -export const useAuth = () => { - const context = useContext(AuthContext); - if (!context) { - throw new Error("useAuth must be used within an AuthProvider"); - } - return context; -}; \ No newline at end of file diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 38cca17..8ca8296 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -3,7 +3,7 @@ import ReactDOM from "react-dom/client"; import App from "./App"; import { GoogleOAuthProvider } from "@react-oauth/google"; import { BrowserRouter } from "react-router-dom"; -import { AuthProvider } from "./hooks/authentication/IsAuthenticated"; +import { AuthProvider } from "./contexts/AuthContextProvider"; const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID; @@ -12,7 +12,7 @@ ReactDOM.createRoot(document.getElementById("root")).render( - +