From 8418520d287c39fbc62ee572d51f4d7fa03f0189 Mon Sep 17 00:00:00 2001 From: a2-imeri <Alfret2.imeri@live.uwe.ac.uk> Date: Fri, 5 Jul 2024 09:44:57 +0100 Subject: [PATCH] Update Token Authentication API --- .../__pycache__/settings.cpython-310.pyc | Bin 4147 -> 4219 bytes MisplaceAI/MisplaceAI/settings.py | 12 +- .../__pycache__/urls.cpython-310.pyc | Bin 600 -> 656 bytes .../__pycache__/views.cpython-310.pyc | Bin 2185 -> 2576 bytes MisplaceAI/authentication/forms.py | 31 ----- MisplaceAI/authentication/serializers.py | 1 - MisplaceAI/authentication/urls.py | 3 +- MisplaceAI/authentication/views.py | 11 +- frontend/package-lock.json | 10 ++ frontend/package.json | 1 + frontend/src/App.js | 8 +- frontend/src/constants.js | 2 + frontend/src/firewall/ProtectedRoute.js | 54 +++++++- frontend/src/firewall/RouteProtection.js | 17 ++- frontend/src/layouts/Navbar/Navbar.js | 8 +- frontend/src/services/api.js | 121 ++++++++++++++---- frontend/src/services/auth.js | 116 ++++++++--------- 17 files changed, 253 insertions(+), 142 deletions(-) delete mode 100644 MisplaceAI/authentication/forms.py create mode 100644 frontend/src/constants.js diff --git a/MisplaceAI/MisplaceAI/__pycache__/settings.cpython-310.pyc b/MisplaceAI/MisplaceAI/__pycache__/settings.cpython-310.pyc index 78bd7a0ef7bd7019670610a6016177d89c9a0e2d..02813be6beff5c02754f819d73ecadb2a17f005a 100644 GIT binary patch delta 330 zcmdn2@LPd5pO=@5fq{YHU~^mA+=;xhjH@PU-;?BsQc96akxx-bQA|-vQBF}wQJuq< z$rz<PS&mV$UL{2@N;O44N-c#WN<D=mN+ZP}N;8!+#Za0dl{3XinjxJjN-M=UN;{Rm z=yD2U3UiK5lungms?Gx46qALF3-lH;q?k%Fq%cY{q?k!EMCqsMFEB_kU&xqZA<3}7 zAjNVaBS<dFFjZiIQL5oWMn;Bkh7{&t1{Q`?oh)OPER)Jhn-4MGXJi!Kyoi~XNwZ3$ zD7CmGKCLJ*H?=&!C_BD5Gq)foHLJWtuOvS^H7`CXCowrYC$o6-I@ZT*jHa6>@jPIZ zh!S)54+@TV^zrcz2a)mqL7wiOe!-iI_@o&bmrU;Cmy#4`;$ivE#LDuYg_VVgg^`7k Wg&9mTPCm%5!otGF#5(yozYhRt3R^Y+ delta 306 zcmeyZuvvjOpO=@5fq{XcAi5=O#zbCO#$^+=@72qt$fqczD5fZ-D5t2TsLo-_WQ<Zu zQA<(JWK7XW(M-|GWK7YX!<3?yqLay(qL!kY$(YU(rJSM{rIMl_rJBMKrIx}GrJiCC zrIE^+Vkpg!%9&y$&5+I%rI}(JrIpHGbTNf7g*it%O1nxiReOO>ipfI81-c6vQcNWo zQW&iy8B)w78KU%3^%m%-m@i~Zv5;g~pr2y7kTHc(k|D|<RbYW(s=-1=Muu>P6y{(C z7KT*qETb&r$_ty1G2Uln6xzIunU`tvUDiizj3%28@H}ALyn|1g(PR+=0|N^K4=)EJ z52FAx3mXd)A0rDh8w&>uhZqwN%YP<Tmj5iQEKDqnEQ~BnEX*J{nO#7IiHU2nzJLz^ D*;7Z_ diff --git a/MisplaceAI/MisplaceAI/settings.py b/MisplaceAI/MisplaceAI/settings.py index 83a2b6a..0319f3b 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 GIT binary patch delta 268 zcmcb?GJ#b)pO=@5fq{Wx{i4>iUM2>H#~=<e2FW@wFfbI)nW$}8pUWA=$;gn(n8KRM zwt#CPLke38doOb;V+uzqJDAT2<#VNSfce}|K2It$n9mF4^QE$Y`TS76Kq@DgF9_nN zvqf>I2n91}3cmz7MU(LsOF?2uh9=X*<GPG26TfP5+~V@dPtPwcnJmTFDpJLs1Lo-8 zVuLXJG<ha3Vhmy`;+*`EQHD*3fq|h&XtFSqzJv$^0|O7E0HXw>0J8ui408Me061AZ A^8f$< delta 224 zcmbQhdV@tfpO=@5fq{XcNWUe`jfsKbF^GeVIT;uj92giFiaRE18^$m)q%x+krm`*I zT*#2Zmcri4oXVKOk;)F{b3*xCsT^QFH<Ztl$_(c7Liv2DEMPuAh@Z|H#g!ru%%CYa z@r(^4^JGp&&B=C*jm&<U+><Xe2Fc%IPb`iv%S<i5#a&vIQ;=9vl3J8kypo|v0OV?c b$(~I5VnPfI3_Oeij3SHz%mR!M$nhHhP$n?? diff --git a/MisplaceAI/authentication/__pycache__/views.cpython-310.pyc b/MisplaceAI/authentication/__pycache__/views.cpython-310.pyc index b5f125413f28b700e37fb77778c585ec39c6ddc8..0f3a5bf6b0e6ff587ef4c85fae26f30fe63f9f50 100644 GIT binary patch delta 912 zcmeAaoFKxN&&$ijz`($8vbinoEayZ%8OB8uwc|b98B#b?xLO!exKf$31e%$n1l<`@ zxKnsq7*cprIhvWHgxncYcvJXV7*hDad|`Kn6#f)}7KRi7Fkd7^FqlD8XyUn+$&O5t zlS>$T*+du^7>YzDvok4)@-Q$ka5FG46tgifFmNz%F>){#@octZl3`?%0jZLmoWLBz zC^>mA^HC{9kN`w4h$YIvz;KJDAiuaoVDcOmJ8NE$ARma}2NA{~0-+kr0%<7{XJB9e zsrbwSwvvajNPMy#lMGXl*kohY^-2;93=APfQXt(B7l2rD3=9mKEJa`!C{1QzQ(#j7 z3CmB`V#|(_1TmFC1QUo*0THSof*nMtg9ug-p#dT^K?KO=A}tV08${@U2(Sae1lW}z z=PONq&*rFMf+~wpV#dJ00CtfnNQD{56ZJ)6V3*$F^2txnFD(hnOf63axe64sAPjN{ zD4Lx?VIIZ6z)&Sp!<fa8!YIj5%T&Tx!_>@J%Ur{}fN3EEBSQ&u2}>4h3R5p*I72W4 z3quK8r4A!QEej(<6%SaB9U{lZ5Xq3jP|H%sQXdRulrxqyXfpfV;w?%|D@rZSh%d>{ zPR%O<g_<Tu5jey^p>vBfDJL;GJ14WaqzL4+B2dWO67vWN35Yi`FpUp#4R#HQclP%S zarFzi#pzm+oLW$lnV(k#3O7yGB3+Qr^+1FrC^fLwgRQy6m|6r5CNKdHPEbG=3o$S- zFfoGC83!XDBOjv#BS!<%|0;e&c<F(XlwT1@HIgX^qbwL07(lU81P*o(&rehImT*c| zVqW^>RQ4k#w|Je4QWHy39RoZ;;a>zw<KR?R1d6dDkPZxkC&zNAa0oF8um}i=2mt^H C?5LUm delta 532 zcmbOr(kaN7&&$ijz`($err(m*!ZDFghH>6R?Rb|Ih7`^mhFpOt0Y(OQh7_(8?iPj= z?o{S1!Di+tA$Nupo)q2|h7{gZj%Ma4VRwcUz7+lzh7^7<UnE5!m_bu;;-wZ&O~zZS zIr-_Cd6R9JBq!%H_OgjGFfbH}PG(|K66IxJVBle3U?^r|U|`^2;9}%pEaIIkz$&vi zl!=i|7Nk&aax`-cqtxUb%txh^Kmri$AeI;d1H&zrg8bqV!O7EE?5z1fg8U#t07RI8 z2!v`d3#6q;f`Nenq~bFR*j66KB8kZYtg?*all55FD@igiFoYCIgET`N0Ak5AFfeGc z6oDO}Jo!JX0-GX8SYfg%TXvKbh^Yc1m_URoh)@F&>>xq|M6iMgO%S04B0v@wX@gig zAVL>JfZYcsz>WmDUU~8>Hb)IpR9S=)a|Q+mu#3z<D$FO}=eTH61Pabuyv{|bi6yCy m0iI!*spUnWU<UiT2&AP5YyyH1oBV`Rg@cDth)IA&L<j&CDq02r diff --git a/MisplaceAI/authentication/forms.py b/MisplaceAI/authentication/forms.py deleted file mode 100644 index ee9fab5..0000000 --- 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 70a5de5..d73b408 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 fdf2fd9..9f611ab 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 e1e96bd..1413323 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 f61fa33..d244df8 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 38d2da0..16a7394 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 231db48..d3e12a1 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 0000000..2ebf897 --- /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 15da355..c1563a9 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 68f399c..6636f20 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 c68b43a..c1d837b 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 26c4cd0..fe9d2f9 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 f0d90d0..b9ea689 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; } }; -- GitLab