diff --git a/MisplaceAI/MisplaceAI/__pycache__/settings.cpython-310.pyc b/MisplaceAI/MisplaceAI/__pycache__/settings.cpython-310.pyc index 3e6038a00a1f6a1ac6472eb94b2cba19f6f3c152..7fa541b2a5a2e158311f32a20c37fca92143ce0d 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 a1a77a2b06e3cef1d7837e6e993f4a828b7ceec7..f16360060b57da255322e9e442a959f7d5b5b2c0 100644 --- a/MisplaceAI/MisplaceAI/settings.py +++ b/MisplaceAI/MisplaceAI/settings.py @@ -13,6 +13,7 @@ https://docs.djangoproject.com/en/4.0/ref/settings/ from pathlib import Path import os +from datetime import timedelta # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -110,6 +111,12 @@ REST_FRAMEWORK = { ], } +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': True, + 'BLACKLIST_AFTER_ROTATION': True, +} # Database # https://docs.djangoproject.com/en/4.0/ref/settings/#databases diff --git a/MisplaceAI/admin_app/urls.py b/MisplaceAI/admin_app/urls.py index ea58610adf9f5eb9b1ab6785307e1aa349e89932..3648c708b3725ea9098d302cd4e882a9d0ccdeb5 100644 --- a/MisplaceAI/admin_app/urls.py +++ b/MisplaceAI/admin_app/urls.py @@ -1,7 +1,6 @@ # MisplaceAI/admin_app/urls.py from django.urls import path from .views import ( - AdminLoginView, admin_dashboard_view, admin_users_view, admin_deactivate_user_view, @@ -10,7 +9,6 @@ from .views import ( ) urlpatterns = [ - path('login/', AdminLoginView.as_view(), name='admin_login'), path('dashboard/', admin_dashboard_view, name='admin_dashboard'), path('users/', admin_users_view, name='admin_users'), path('users/deactivate/<int:user_id>/', admin_deactivate_user_view, name='admin_deactivate_user'), diff --git a/MisplaceAI/admin_app/views.py b/MisplaceAI/admin_app/views.py index 5e137a330e39b1cb79e2f15d63a3d17d431191f6..46dd4a4bc742dc62758acd2925e06dbb19babfed 100644 --- a/MisplaceAI/admin_app/views.py +++ b/MisplaceAI/admin_app/views.py @@ -12,22 +12,6 @@ from django.contrib.auth.decorators import login_required from .serializers import UserSerializer -class AdminLoginView(APIView): - def post(self, request, *args, **kwargs): - username = request.data.get('username') - password = request.data.get('password') - user = authenticate(request, username=username, password=password) - if user is not None and user.is_superuser: - login(request, user) - refresh = RefreshToken.for_user(user) - return Response({ - 'refresh': str(refresh), - 'access': str(refresh.access_token), - 'is_admin': True, - '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) - @api_view(['GET']) @permission_classes([IsAuthenticated]) def admin_dashboard_view(request): diff --git a/MisplaceAI/authentication/__pycache__/urls.cpython-310.pyc b/MisplaceAI/authentication/__pycache__/urls.cpython-310.pyc index 8bb1713bf934e8764d4b1e4c353a79cd98bf0961..1008e47ecfc3b133b5260926e8c9ad6097420ed4 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 5fe73a8a01b42fc98c48199de2c648f55a70125c..b5f125413f28b700e37fb77778c585ec39c6ddc8 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/urls.py b/MisplaceAI/authentication/urls.py index aeb63868587b9405b02b869c8511cd8106fa1035..fdf2fd9f803bf9c8505553e8bea142202e88be9a 100644 --- a/MisplaceAI/authentication/urls.py +++ b/MisplaceAI/authentication/urls.py @@ -1,9 +1,12 @@ # MisplaceAI/authentication/urls.py from django.urls import path -from .views import RegisterView, LoginView +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView +from .views import RegisterView, LoginView, AdminLoginView urlpatterns = [ path('register/', RegisterView.as_view(), name='register'), path('login/', LoginView.as_view(), name='login'), - + 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'), ] diff --git a/MisplaceAI/authentication/views.py b/MisplaceAI/authentication/views.py index f0eb4cd8c9d971363aa875bbd9d97a07d5e87139..e1e96bd4e4fefa4bc8c98c4ee3c1748681764365 100644 --- a/MisplaceAI/authentication/views.py +++ b/MisplaceAI/authentication/views.py @@ -1,4 +1,5 @@ # MisplaceAI/authentication/views.py + from django.contrib.auth import authenticate from rest_framework import generics, status from rest_framework.response import Response @@ -6,6 +7,7 @@ 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() @@ -29,5 +31,20 @@ class LoginView(APIView): }, status=status.HTTP_200_OK) return Response({'error': 'Invalid credentials'}, status=status.HTTP_400_BAD_REQUEST) - - \ No newline at end of file +class AdminLoginView(APIView): + def post(self, request, *args, **kwargs): + serializer = LoginSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = authenticate( + username=serializer.validated_data['username'], + password=serializer.validated_data['password'] + ) + if user and user.is_superuser: + refresh = RefreshToken.for_user(user) + return Response({ + 'refresh': str(refresh), + 'access': str(refresh.access_token), + 'is_admin': True, + '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) diff --git a/frontend/src/forms/Auth/AdminLoginForm.js b/frontend/src/forms/Auth/AdminLoginForm.js index 6899fa30c198f25ad27e93af333ae622f9ca7e1a..3e295dcb2fd3950ffd23bcae39a2cc66e9cb1e73 100644 --- a/frontend/src/forms/Auth/AdminLoginForm.js +++ b/frontend/src/forms/Auth/AdminLoginForm.js @@ -1,7 +1,7 @@ // src/forms/Auth/AdminLoginForm.js import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import api from '../../services/api'; +import { adminLogin } from '../../services/auth'; // Import the adminLogin function import FormContainer from '../../components/Common/Form/FormContainer'; import FormField from '../../components/Common/Form/FormField'; import PasswordInputField from '../../components/Common/Password/PasswordInputField'; @@ -18,11 +18,7 @@ const AdminLoginForm = () => { const handleSubmit = async (e) => { e.preventDefault(); try { - const response = await api.post('/api/admin-app/login/', { username, password }); - localStorage.setItem('adminToken', response.data.access); - localStorage.setItem('isAdmin', true); - localStorage.setItem('username', username); - localStorage.setItem('isAuthenticated', true); + await adminLogin({ username, password }); navigate('/admin/dashboard'); window.location.reload(); // Refresh to update Navbar } catch (error) { diff --git a/frontend/src/forms/Auth/LoginForm.js b/frontend/src/forms/Auth/LoginForm.js index 930e31c8ce8c6141032dcdcae7ad6ed1cabf5c59..3c010c725323314a1e6e56551958cba8fa35841c 100644 --- a/frontend/src/forms/Auth/LoginForm.js +++ b/frontend/src/forms/Auth/LoginForm.js @@ -1,8 +1,7 @@ // src/forms/Auth/LoginForm.js - import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import api from '../../services/api'; +import { login } from '../../services/auth'; import FormContainer from '../../components/Common/Form/FormContainer'; import FormField from '../../components/Common/Form/FormField'; import PasswordInputField from '../../components/Common/Password/PasswordInputField'; @@ -19,10 +18,7 @@ const LoginForm = () => { const handleSubmit = async (e) => { e.preventDefault(); try { - const response = await api.post('/api/auth/login/', { username, password }); - localStorage.setItem('token', response.data.access); - localStorage.setItem('username', username); - localStorage.setItem('isAuthenticated', true); + await login({ username, password }); navigate('/'); window.location.reload(); // Refresh to update Navbar } catch (error) { diff --git a/frontend/src/forms/Auth/RegisterForm.js b/frontend/src/forms/Auth/RegisterForm.js index 41e5e8a1322d711bcb357f6af3d2e9177987c52e..df18d2ea6a6902cdc09b4ba0ed0f057332af3294 100644 --- a/frontend/src/forms/Auth/RegisterForm.js +++ b/frontend/src/forms/Auth/RegisterForm.js @@ -1,7 +1,7 @@ // src/forms/Auth/RegisterForm.js import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import api from '../../services/api'; +import { register } from '../../services/auth'; import FormContainer from '../../components/Common/Form/FormContainer'; import FormField from '../../components/Common/Form/FormField'; import PasswordInputField from '../../components/Common/Password/PasswordInputField'; @@ -79,10 +79,7 @@ const RegisterForm = () => { return; } try { - const response = await api.post('/api/auth/register/', { username, email, password, password2 }); - localStorage.setItem('token', response.data.access); - localStorage.setItem('username', username); - localStorage.setItem('isAuthenticated', true); + await register({ username, email, password }); navigate('/'); window.location.reload(); // Refresh to update Navbar } catch (error) { diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 5e6e9d190c7ea233337be534bf2e769768c31be4..d74f49ba036acb4b2b6729af244ce1a5635ac3d4 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -1,5 +1,6 @@ // src/services/api.js import axios from 'axios'; +import { refreshToken } from './auth'; // Import the refresh token function export const getCsrfToken = () => { const cookies = document.cookie.split(';'); @@ -13,25 +14,41 @@ export const getCsrfToken = () => { }; const api = axios.create({ - baseURL: 'http://localhost:8080', // Update this to match your Django backend address + baseURL: 'http://localhost:8080', headers: { 'Content-Type': 'application/json', }, }); api.interceptors.request.use( - config => { - const token = localStorage.getItem('token') || localStorage.getItem('adminToken'); - const csrfToken = getCsrfToken(); + async (config) => { + let token = localStorage.getItem('token') || localStorage.getItem('adminToken'); if (token) { + const tokenExpiry = localStorage.getItem('tokenExpiry'); + const now = Math.floor(Date.now() / 1000); + if (tokenExpiry && now >= tokenExpiry) { + // Token expired, refresh it + const newTokens = await refreshToken(); + if (newTokens) { + token = newTokens.access; + } else { + // Refresh token also expired or failed, logout user + localStorage.removeItem('token'); + localStorage.removeItem('refresh'); + localStorage.removeItem('tokenExpiry'); + window.location.href = '/login'; + return Promise.reject('Session expired. Please log in again.'); + } + } config.headers.Authorization = `Bearer ${token}`; } + const csrfToken = getCsrfToken(); if (csrfToken) { config.headers['X-CSRFToken'] = csrfToken; } return config; }, - error => Promise.reject(error) + (error) => Promise.reject(error) ); export default api; diff --git a/frontend/src/services/auth.js b/frontend/src/services/auth.js index 73b599b7cf1727afde884b212b4354564dd5e2ca..42da5181c94f5e8c75d253f01d2690af406e4f57 100644 --- a/frontend/src/services/auth.js +++ b/frontend/src/services/auth.js @@ -5,6 +5,9 @@ export const login = async (credentials) => { 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; @@ -12,15 +15,53 @@ export const login = async (credentials) => { export const register = async (userData) => { 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; }; export const logout = () => { localStorage.removeItem('token'); + localStorage.removeItem('refresh'); + localStorage.removeItem('tokenExpiry'); localStorage.removeItem('isAuthenticated'); }; +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) { + logout(); + } + } + return null; +}; + export const getCurrentUser = async () => { const response = await api.get('/api/auth/user/'); return response.data; }; + + +export const adminLogin = async (credentials) => { + 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); + } + return response.data; +} +