diff --git a/MisplaceAI/MisplaceAI/__pycache__/settings.cpython-310.pyc b/MisplaceAI/MisplaceAI/__pycache__/settings.cpython-310.pyc index 78bd7a0ef7bd7019670610a6016177d89c9a0e2d..02813be6beff5c02754f819d73ecadb2a17f005a 100644 Binary files a/MisplaceAI/MisplaceAI/__pycache__/settings.cpython-310.pyc and b/MisplaceAI/MisplaceAI/__pycache__/settings.cpython-310.pyc differ diff --git a/MisplaceAI/MisplaceAI/settings.py b/MisplaceAI/MisplaceAI/settings.py index 83a2b6a54277550838309951ace4ee0061ee9e2c..0319f3b179831fcde8418fed69f6c084058385d4 100644 --- a/MisplaceAI/MisplaceAI/settings.py +++ b/MisplaceAI/MisplaceAI/settings.py @@ -87,14 +87,9 @@ ROOT_URLCONF = 'MisplaceAI.urls' SESSION_COOKIE_AGE = 1209600 # 2 weeks in seconds -SESSION_EXPIRE_AT_BROWSER_CLOSE = False# Do not expire the session when the browser closes - - -# Optionally, secure the session cookie (recommended for production) -SESSION_COOKIE_SECURE = False # Set to True if using HTTPS - -# Save the session cookie on every request (optional, based on your needs) -SESSION_SAVE_EVERY_REQUEST = True +SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Do not expire the session when the browser closes +SESSION_COOKIE_SECURE = False # Set to True if using HTTPS +SESSION_SAVE_EVERY_REQUEST = True # Save the session cookie on every request (optional, based on your needs) # Configuration of the session engine SESSION_ENGINE = 'django.contrib.sessions.backends.db' @@ -135,6 +130,7 @@ SIMPLE_JWT = { 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, } +CORS_ALLOW_ALL_ORIGINS = True # Database # https://docs.djangoproject.com/en/4.0/ref/settings/#databases diff --git a/MisplaceAI/authentication/__pycache__/urls.cpython-310.pyc b/MisplaceAI/authentication/__pycache__/urls.cpython-310.pyc index 1008e47ecfc3b133b5260926e8c9ad6097420ed4..b79f85e6c3da732f08f20763d8d9dd4f9f58c0c9 100644 Binary files a/MisplaceAI/authentication/__pycache__/urls.cpython-310.pyc and b/MisplaceAI/authentication/__pycache__/urls.cpython-310.pyc differ diff --git a/MisplaceAI/authentication/__pycache__/views.cpython-310.pyc b/MisplaceAI/authentication/__pycache__/views.cpython-310.pyc index b5f125413f28b700e37fb77778c585ec39c6ddc8..0f3a5bf6b0e6ff587ef4c85fae26f30fe63f9f50 100644 Binary files a/MisplaceAI/authentication/__pycache__/views.cpython-310.pyc and b/MisplaceAI/authentication/__pycache__/views.cpython-310.pyc differ diff --git a/MisplaceAI/authentication/forms.py b/MisplaceAI/authentication/forms.py deleted file mode 100644 index ee9fab549ff88ab9af1f54f2c13cfe4c8faffc42..0000000000000000000000000000000000000000 --- a/MisplaceAI/authentication/forms.py +++ /dev/null @@ -1,31 +0,0 @@ -# MisplaceAI/authentication/forms.py - -from django import forms -from django.contrib.auth.forms import UserCreationForm, AuthenticationForm -from django.contrib.auth.models import User - -class RegisterForm(UserCreationForm): - email = forms.EmailField() - - class Meta: - model = User - fields = ['username', 'email', 'password1', 'password2'] - -class LoginForm(AuthenticationForm): - username = forms.CharField(label='Username') - password = forms.CharField(widget=forms.PasswordInput) - - -class CustomUserCreationForm(UserCreationForm): - email = forms.EmailField(required=True, widget=forms.EmailInput(attrs={'class': 'form-control'})) - - class Meta: - model = User - fields = ("username", "email", "password1", "password2") - - def save(self, commit=True): - user = super(CustomUserCreationForm, self).save(commit=False) - user.email = self.cleaned_data["email"] - if commit: - user.save() - return user \ No newline at end of file diff --git a/MisplaceAI/authentication/serializers.py b/MisplaceAI/authentication/serializers.py index 70a5de55026553c7a3a725066f12550ead1d404b..d73b408511c083bda54efc58073f53321ca938b7 100644 --- a/MisplaceAI/authentication/serializers.py +++ b/MisplaceAI/authentication/serializers.py @@ -3,7 +3,6 @@ from django.contrib.auth import get_user_model from rest_framework import serializers -from django.contrib.auth.models import User from django.contrib.auth import authenticate UserModel = get_user_model() diff --git a/MisplaceAI/authentication/urls.py b/MisplaceAI/authentication/urls.py index fdf2fd9f803bf9c8505553e8bea142202e88be9a..9f611ab4f098b3be27e4b264918bc13a2398343c 100644 --- a/MisplaceAI/authentication/urls.py +++ b/MisplaceAI/authentication/urls.py @@ -1,7 +1,7 @@ # MisplaceAI/authentication/urls.py from django.urls import path from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView -from .views import RegisterView, LoginView, AdminLoginView +from .views import RegisterView, LoginView, AdminLoginView, LogoutView urlpatterns = [ path('register/', RegisterView.as_view(), name='register'), @@ -9,4 +9,5 @@ urlpatterns = [ path('admin/login/', AdminLoginView.as_view(), name='admin_login'), path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('logout/', LogoutView.as_view(), name='logout'), ] diff --git a/MisplaceAI/authentication/views.py b/MisplaceAI/authentication/views.py index e1e96bd4e4fefa4bc8c98c4ee3c1748681764365..1413323f7e0b263d0898e95ec86b3731b6218f43 100644 --- a/MisplaceAI/authentication/views.py +++ b/MisplaceAI/authentication/views.py @@ -7,7 +7,6 @@ from rest_framework.views import APIView from rest_framework_simplejwt.tokens import RefreshToken from .serializers import RegisterSerializer, LoginSerializer from django.contrib.auth.models import User -from django.contrib.auth import login class RegisterView(generics.CreateAPIView): queryset = User.objects.all() @@ -48,3 +47,13 @@ class AdminLoginView(APIView): 'is_authenticated': True }, status=status.HTTP_200_OK) return Response({'error': 'Invalid username or password or you do not have the necessary permissions to access this page.'}, status=status.HTTP_400_BAD_REQUEST) + +class LogoutView(APIView): + def post(self, request): + try: + refresh_token = request.data["refresh_token"] + token = RefreshToken(refresh_token) + token.blacklist() + return Response(status=status.HTTP_205_RESET_CONTENT) + except Exception as e: + return Response(status=status.HTTP_400_BAD_REQUEST) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f61fa330bb05180ed216c4a336e8de4a3e207183..d244df8830336d2fe0a6d81187359350c2aad44b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "axios": "^1.7.2", "bootstrap": "^5.3.3", "font-awesome": "^4.7.0", + "jwt-decode": "^4.0.0", "react": "^18.0.0", "react-bootstrap": "^2.10.2", "react-dom": "^18.0.0", @@ -13134,6 +13135,15 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 38d2da0075a6b72d0b4f09646d28d41d46692cc6..16a73943ae9a82518b45318ba0934a72ebd36e1f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,7 @@ "axios": "^1.7.2", "bootstrap": "^5.3.3", "font-awesome": "^4.7.0", + "jwt-decode": "^4.0.0", "react": "^18.0.0", "react-bootstrap": "^2.10.2", "react-dom": "^18.0.0", diff --git a/frontend/src/App.js b/frontend/src/App.js index 231db48325ababe277aa8fa5a27c8bd694548f7c..d3e12a1347b1545680d662613734f1b28aaeffb8 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,5 +1,6 @@ // src/App.js -import React from 'react'; + +import React, { useEffect } from 'react'; import { BrowserRouter as Router, Route, Routes, useLocation } from 'react-router-dom'; import Navbar from './layouts/Navbar/Navbar'; import HomePage from './pages/Home/Home'; @@ -21,10 +22,15 @@ import ManageDailyLimit from './pages/Admin/ManageDailyLimit'; import Error500 from './pages/Error/Error500/Error500'; import ProtectedRoute from './firewall/ProtectedRoute'; import RouteProtection from './firewall/RouteProtection'; +import { initializeTokenRefresh } from './services/api'; // Import the initializeTokenRefresh function function App() { const location = useLocation(); + useEffect(() => { + initializeTokenRefresh(); // Initialize token refresh timer on app load + }, []); + return ( <div className="App"> {location.pathname !== '/error-500' && <Navbar />} diff --git a/frontend/src/constants.js b/frontend/src/constants.js new file mode 100644 index 0000000000000000000000000000000000000000..2ebf897789c0104a7efbd0fe69355b76e7c12b73 --- /dev/null +++ b/frontend/src/constants.js @@ -0,0 +1,2 @@ +export const ACCESS_TOKEN = "access"; +export const REFRESH_TOKEN = "refresh"; diff --git a/frontend/src/firewall/ProtectedRoute.js b/frontend/src/firewall/ProtectedRoute.js index 15da355a660f657be8c211b9334eb0fad5cce17b..c1563a969fbc11fe324cbdf872c1b2017798a5d0 100644 --- a/frontend/src/firewall/ProtectedRoute.js +++ b/frontend/src/firewall/ProtectedRoute.js @@ -1,17 +1,57 @@ // src/firewall/ProtectedRoute.js -import React from 'react'; + +import React, { useState, useEffect } from 'react'; import { Navigate } from 'react-router-dom'; +import { jwtDecode } from 'jwt-decode'; +import { ACCESS_TOKEN } from '../constants'; const ProtectedRoute = ({ children, isAdminRoute }) => { - const isAuthenticated = !!localStorage.getItem('isAuthenticated'); - const isAdmin = !!localStorage.getItem('isAdmin'); + const [isAuthorized, setIsAuthorized] = useState(null); - if (!isAuthenticated) { - return <Navigate to="/login" replace />; + useEffect(() => { + const auth = async () => { + const token = localStorage.getItem(ACCESS_TOKEN); + + if (!token) { + console.log('No access token found, redirecting to login'); + setIsAuthorized(false); + return; + } + + try { + const decoded = jwtDecode(token); + const tokenExpiration = decoded.exp; + const now = Math.floor(Date.now() / 1000); + + if (tokenExpiration < now) { + console.log('Access token expired, redirecting to login'); + setIsAuthorized(false); + } else { + if (isAdminRoute) { + const isAdmin = localStorage.getItem('isAdmin'); + if (!isAdmin) { + console.log('Not an admin, redirecting to home'); + setIsAuthorized(false); + return; + } + } + setIsAuthorized(true); + } + } catch (error) { + console.log('Error decoding token:', error); + setIsAuthorized(false); + } + }; + + auth().catch(() => setIsAuthorized(false)); + }, [isAdminRoute]); + + if (isAuthorized === null) { + return <div>Loading...</div>; } - if (isAdminRoute && !isAdmin) { - return <Navigate to="/" replace />; + if (!isAuthorized) { + return <Navigate to="/login" replace />; } return children; diff --git a/frontend/src/firewall/RouteProtection.js b/frontend/src/firewall/RouteProtection.js index 68f399c06738a150de977b331cbda68f5dae86ff..6636f20745c3a56d4c136cf361012bb0b2f85202 100644 --- a/frontend/src/firewall/RouteProtection.js +++ b/frontend/src/firewall/RouteProtection.js @@ -1,9 +1,24 @@ // src/firewall/RouteProtection.js + import React from 'react'; import { Outlet, Navigate } from 'react-router-dom'; +import { jwtDecode } from 'jwt-decode'; +import { ACCESS_TOKEN } from '../constants'; const RouteProtection = ({ redirectTo }) => { - const isAuthenticated = !!localStorage.getItem('isAuthenticated'); + const token = localStorage.getItem(ACCESS_TOKEN); + let isAuthenticated = false; + + if (token) { + try { + const decoded = jwtDecode(token); + const tokenExpiration = decoded.exp; + const now = Date.now() / 1000; + isAuthenticated = tokenExpiration > now; + } catch (error) { + console.error("Invalid token"); + } + } return isAuthenticated ? <Navigate to={redirectTo} replace /> : <Outlet />; }; diff --git a/frontend/src/layouts/Navbar/Navbar.js b/frontend/src/layouts/Navbar/Navbar.js index c68b43a66711a986f1e113cd78c8318f74491c2f..c1d837b0c5e9813b001f4811c77ebfe0d7aa3484 100644 --- a/frontend/src/layouts/Navbar/Navbar.js +++ b/frontend/src/layouts/Navbar/Navbar.js @@ -2,17 +2,15 @@ import React from 'react'; import { Link, useNavigate } from 'react-router-dom'; import '../../styles/main.css'; +import { logout } from '../../services/auth'; const Navbar = () => { const navigate = useNavigate(); const isAuthenticated = !!localStorage.getItem('isAuthenticated'); const isAdmin = !!localStorage.getItem('isAdmin'); - const handleLogout = () => { - localStorage.removeItem('token'); - localStorage.removeItem('adminToken'); - localStorage.removeItem('isAuthenticated'); - localStorage.removeItem('isAdmin'); + const handleLogout = async () => { + await logout(); navigate('/'); window.location.reload(); // Refresh to update Navbar }; diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 26c4cd0ebb1a1f3354b2718f6a8cd3de367e72a4..fe9d2f9f1efef552c80989a33fc76ed8642011af 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -1,9 +1,11 @@ // src/services/api.js import axios from 'axios'; -import { refreshToken, logout } from './auth'; +import { jwtDecode } from 'jwt-decode'; +import { ACCESS_TOKEN, REFRESH_TOKEN } from '../constants'; +import { logout } from './auth'; // Importing the consolidated logout -export const getCsrfToken = () => { +const getCsrfToken = () => { const cookies = document.cookie.split(';'); for (let cookie of cookies) { const [name, value] = cookie.split('='); @@ -19,41 +21,107 @@ const api = axios.create({ headers: { 'Content-Type': 'application/json', }, - withCredentials: true, // Ensure credentials are sent with requests + withCredentials: true, }); +let isRefreshing = false; + +const setRefreshTimer = (expiryTime) => { + const now = Math.floor(Date.now() / 1000); + const timeToRefresh = (expiryTime - now - 60) * 1000; // Set timer to 1 minute before expiry + setTimeout(() => { + if (!isRefreshing) { + refreshToken(); + } + }, timeToRefresh); +}; + +const refreshToken = async () => { + if (isRefreshing) { + return; // If a refresh is already in progress, don't start another one + } + isRefreshing = true; + const refresh = localStorage.getItem(REFRESH_TOKEN); + if (refresh) { + try { + console.log('Attempting to refresh token with refresh token:', refresh); + const response = await api.post('/api/auth/token/refresh/', { refresh }); + const { access, refresh: newRefreshToken } = response.data; + + console.log('Access Token:', access); + localStorage.setItem(ACCESS_TOKEN, access); + + // If a new refresh token is provided, save it + if (newRefreshToken) { + console.log('New Refresh Token:', newRefreshToken); + localStorage.setItem(REFRESH_TOKEN, newRefreshToken); + } + + setRefreshTimer(jwtDecode(access).exp); // Set the timer after refreshing the token + } catch (error) { + console.log('Token refresh error:', error); + logout(); + } finally { + isRefreshing = false; + } + } else { + logout(); + isRefreshing = false; + } +}; + +const initializeTokenRefresh = () => { + const token = localStorage.getItem(ACCESS_TOKEN); + const refresh = localStorage.getItem(REFRESH_TOKEN); + + if (token) { + const decoded = jwtDecode(token); + const tokenExpiration = decoded.exp; + const now = Math.floor(Date.now() / 1000); + + if (tokenExpiration > now) { + setRefreshTimer(tokenExpiration); + } else if (refresh) { + refreshToken(); // Token expired, but refresh token exists + } + } else if (refresh) { + refreshToken(); // No access token, but refresh token exists + } +}; + +const obtainToken = async (url, credentials) => { + try { + const response = await api.post(url, credentials); + const { access, refresh } = response.data; + + console.log('Access Token:', access); + localStorage.setItem(ACCESS_TOKEN, access); + localStorage.setItem(REFRESH_TOKEN, refresh); + + setRefreshTimer(jwtDecode(access).exp); // Set the timer after obtaining the token + + return response.data; + } catch (error) { + console.log('Token obtain error:', error); + throw error; + } +}; + api.interceptors.request.use( async (config) => { - let token = localStorage.getItem('token') || localStorage.getItem('adminToken'); + const token = localStorage.getItem(ACCESS_TOKEN); + if (token) { - const tokenExpiry = localStorage.getItem('tokenExpiry'); - const now = Math.floor(Date.now() / 1000); - if (tokenExpiry && now >= tokenExpiry) { - try { - const newTokens = await refreshToken(); - if (newTokens) { - token = newTokens.access; - localStorage.setItem('token', newTokens.access); - localStorage.setItem('tokenExpiry', newTokens.accessExpiry); - } else { - console.log('Token refresh failed. Logging out...'); - logout(); - window.location.href = '/login'; - return Promise.reject('Session expired. Please log in again.'); - } - } catch (err) { - console.log('Error during token refresh:', err); - logout(); - window.location.href = '/login'; - return Promise.reject('Session expired. Please log in again.'); - } - } config.headers.Authorization = `Bearer ${token}`; + } else { + console.log('No access token found for the request'); } + const csrfToken = getCsrfToken(); if (csrfToken) { config.headers['X-CSRFToken'] = csrfToken; } + return config; }, (error) => { @@ -76,4 +144,5 @@ api.interceptors.response.use( } ); +export { getCsrfToken, setRefreshTimer, initializeTokenRefresh, refreshToken, obtainToken }; export default api; diff --git a/frontend/src/services/auth.js b/frontend/src/services/auth.js index f0d90d0c339407b105eed9010d52ef43f036f25d..b9ea689b3e08c99f24d6676f44a0d081e12f2810 100644 --- a/frontend/src/services/auth.js +++ b/frontend/src/services/auth.js @@ -1,70 +1,55 @@ // src/services/auth.js -import api from './api'; +import api, { obtainToken, setRefreshTimer } from './api'; +import { ACCESS_TOKEN, REFRESH_TOKEN } from '../constants'; +import { jwtDecode } from 'jwt-decode'; // Corrected import statement + +// Login function for regular users export const login = async (credentials) => { - try { - const response = await api.post('/api/auth/login/', credentials); - if (response.data.access) { - localStorage.setItem('token', response.data.access); - localStorage.setItem('refresh', response.data.refresh); - const tokenPayload = JSON.parse(atob(response.data.access.split('.')[1])); - localStorage.setItem('tokenExpiry', tokenPayload.exp); - localStorage.setItem('isAuthenticated', true); - } - return response.data; - } catch (error) { - console.log('Login error:', error); - throw error; - } + const data = await obtainToken('/api/auth/login/', credentials); + localStorage.setItem('isAuthenticated', true); + return data; }; +// Register function for new users export const register = async (userData) => { - try { - const response = await api.post('/api/auth/register/', userData); - if (response.data.access) { - localStorage.setItem('token', response.data.access); - localStorage.setItem('refresh', response.data.refresh); - const tokenPayload = JSON.parse(atob(response.data.access.split('.')[1])); - localStorage.setItem('tokenExpiry', tokenPayload.exp); - localStorage.setItem('isAuthenticated', true); - } - return response.data; - } catch (error) { - console.log('Registration error:', error); - throw error; - } + const data = await obtainToken('/api/auth/register/', userData); + localStorage.setItem('isAuthenticated', true); + return data; }; -export const logout = () => { - localStorage.removeItem('token'); - localStorage.removeItem('refresh'); - localStorage.removeItem('tokenExpiry'); - localStorage.removeItem('isAuthenticated'); - localStorage.removeItem('adminToken'); - localStorage.removeItem('isAdmin'); - localStorage.removeItem('username'); +// Admin login function +export const adminLogin = async (credentials) => { + const data = await obtainToken('/api/auth/admin/login/', credentials); + localStorage.setItem('isAdmin', true); + localStorage.setItem('username', credentials.username); + localStorage.setItem('isAuthenticated', true); + return data; }; -export const refreshToken = async () => { - const refresh = localStorage.getItem('refresh'); - if (refresh) { - try { - const response = await api.post('/api/auth/token/refresh/', { refresh }); - localStorage.setItem('token', response.data.access); - const tokenPayload = JSON.parse(atob(response.data.access.split('.')[1])); - localStorage.setItem('tokenExpiry', tokenPayload.exp); - return response.data; - } catch (error) { - console.log('Token refresh error:', error); - logout(); - return null; - } + +// Logout function +export const logout = async () => { + const refreshToken = localStorage.getItem(REFRESH_TOKEN); + try { + console.log('Attempting to logout with refresh token:', refreshToken); + await api.post('/api/auth/logout/', { refresh_token: refreshToken }); + } catch (error) { + console.error('Logout failed:', error); + } finally { + console.log('Clearing local storage'); + localStorage.removeItem(ACCESS_TOKEN); + localStorage.removeItem(REFRESH_TOKEN); + localStorage.removeItem('isAuthenticated'); + localStorage.removeItem('isAdmin'); + localStorage.removeItem('username'); } - return null; }; +// Function to get the current user export const getCurrentUser = async () => { try { + console.log('Fetching current user'); const response = await api.get('/api/auth/user/'); return response.data; } catch (error) { @@ -73,18 +58,29 @@ export const getCurrentUser = async () => { } }; -export const adminLogin = async (credentials) => { +// Function to refresh the token manually if needed +export const refreshToken = async () => { try { - const response = await api.post('/api/auth/admin/login/', credentials); - if (response.data.access) { - localStorage.setItem('adminToken', response.data.access); - localStorage.setItem('isAdmin', true); - localStorage.setItem('username', credentials.username); - localStorage.setItem('isAuthenticated', true); + const refreshToken = localStorage.getItem(REFRESH_TOKEN); + if (!refreshToken) { + throw new Error('No refresh token found'); + } + const response = await api.post('/api/auth/token/refresh/', { refresh: refreshToken }); + const { access, refresh: newRefreshToken } = response.data; + + console.log('Access Token:', access); + localStorage.setItem(ACCESS_TOKEN, access); + + if (newRefreshToken) { + console.log('New Refresh Token:', newRefreshToken); + localStorage.setItem(REFRESH_TOKEN, newRefreshToken); } + + setRefreshTimer(jwtDecode(access).exp); // Set the timer after refreshing the token + return response.data; } catch (error) { - console.log('Admin login error:', error); + console.log('Token refresh error:', error); throw error; } };