diff --git a/config/env/dev.env.example b/config/env/dev.env.example index 4b40294b..59713f64 100644 --- a/config/env/dev.env.example +++ b/config/env/dev.env.example @@ -31,6 +31,7 @@ SQL_PORT=5432 # SQL_SSL_MODE=require LOGIN_REDIRECT_URL= +CORS_ALLOWED_ORIGINS=http://localhost:3000 OPENAI_API_KEY= ANTHROPIC_API_KEY= PINECONE_API_KEY= diff --git a/frontend/src/components/ProtectedRoute/AdminRoute.tsx b/frontend/src/components/ProtectedRoute/AdminRoute.tsx new file mode 100644 index 00000000..61195cb8 --- /dev/null +++ b/frontend/src/components/ProtectedRoute/AdminRoute.tsx @@ -0,0 +1,38 @@ +import { ReactNode, useEffect } from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; +import { useSelector, useDispatch } from 'react-redux'; +import { RootState } from '../../services/actions/types'; +import { AppDispatch, checkAuthenticated } from '../../services/actions/auth'; +import Spinner from '../LoadingSpinner/LoadingSpinner'; + +interface AdminRouteProps { + children: ReactNode; +} + +const AdminRoute = ({ children }: AdminRouteProps) => { + const location = useLocation(); + const dispatch = useDispatch(); + const { isAuthenticated, isSuperuser } = useSelector((state: RootState) => state.auth); + + useEffect(() => { + if (isAuthenticated === null) { + dispatch(checkAuthenticated()); + } + }, [dispatch, isAuthenticated]); + + if (isAuthenticated === null) { + return ; + } + + if (!isAuthenticated) { + return ; + } + + if (!isSuperuser) { + return ; + } + + return children; +}; + +export default AdminRoute; diff --git a/frontend/src/routes/routes.tsx b/frontend/src/routes/routes.tsx index dc974e85..9dd99e97 100644 --- a/frontend/src/routes/routes.tsx +++ b/frontend/src/routes/routes.tsx @@ -19,6 +19,7 @@ import ListofFiles from "../pages/Files/ListOfFiles.tsx"; import RulesManager from "../pages/RulesManager/RulesManager.tsx"; import ManageMeds from "../pages/ManageMeds/ManageMeds.tsx"; import ProtectedRoute from "../components/ProtectedRoute/ProtectedRoute.tsx"; +import AdminRoute from "../components/ProtectedRoute/AdminRoute.tsx"; const routes = [ { @@ -28,17 +29,17 @@ const routes = [ }, { path: "listoffiles", - element: , + element: , errorElement: , }, { path: "rulesmanager", - element: , + element: , errorElement: , }, { path: "uploadfile", - element: , + element: , }, { path: "drugSummary", @@ -86,11 +87,11 @@ const routes = [ }, { path: "adminportal", - element: , + element: , }, { path: "Settings", - element: , + element: , }, { path: "medications", @@ -98,7 +99,7 @@ const routes = [ }, { path: "managemeds", - element: , + element: , }, ]; diff --git a/frontend/src/services/reducers/auth.ts b/frontend/src/services/reducers/auth.ts index 769f3071..9cc5d278 100644 --- a/frontend/src/services/reducers/auth.ts +++ b/frontend/src/services/reducers/auth.ts @@ -68,12 +68,15 @@ const initialState: StateType = { export default function authReducer(state = initialState, action: ActionType): StateType { switch(action.type) { - case AUTHENTICATED_SUCCESS: + case AUTHENTICATED_SUCCESS: { + const token = localStorage.getItem('access'); + const decoded: TokenClaims = token ? jwtDecode(token) : { is_superuser: false }; return { ...state, isAuthenticated: true, - isSuperuser: true + isSuperuser: decoded.is_superuser } + } case LOGIN_SUCCESS: case GOOGLE_AUTH_SUCCESS: case FACEBOOK_AUTH_SUCCESS:{ diff --git a/server/api/permissions.py b/server/api/permissions.py new file mode 100644 index 00000000..0dbe0597 --- /dev/null +++ b/server/api/permissions.py @@ -0,0 +1,6 @@ +from rest_framework.permissions import BasePermission + + +class IsSuperUser(BasePermission): + def has_permission(self, request, view): + return bool(request.user and request.user.is_authenticated and request.user.is_superuser) diff --git a/server/api/views/ai_settings/views.py b/server/api/views/ai_settings/views.py index 9ee6aad7..7f453200 100644 --- a/server/api/views/ai_settings/views.py +++ b/server/api/views/ai_settings/views.py @@ -1,6 +1,6 @@ from rest_framework import status from rest_framework.decorators import api_view, permission_classes -from rest_framework.permissions import IsAuthenticated +from api.permissions import IsSuperUser from rest_framework.response import Response from drf_spectacular.utils import extend_schema from .models import AI_Settings @@ -9,7 +9,7 @@ @extend_schema(request=AISettingsSerializer, responses={200: AISettingsSerializer(many=True), 201: AISettingsSerializer}) @api_view(['GET', 'POST']) -@permission_classes([IsAuthenticated]) +@permission_classes([IsSuperUser]) def settings_view(request): if request.method == 'GET': settings = AI_Settings.objects.all() diff --git a/server/api/views/listMeds/views.py b/server/api/views/listMeds/views.py index 1b199a7e..4321615d 100644 --- a/server/api/views/listMeds/views.py +++ b/server/api/views/listMeds/views.py @@ -1,5 +1,6 @@ from rest_framework import status, serializers as drf_serializers from rest_framework.permissions import AllowAny +from api.permissions import IsSuperUser from rest_framework.response import Response from rest_framework.views import APIView from drf_spectacular.utils import extend_schema, inline_serializer @@ -127,6 +128,7 @@ class AddMedication(APIView): """ API endpoint to add a medication to the database with its risks and benefits. """ + permission_classes = [IsSuperUser] serializer_class = MedicationSerializer def post(self, request): @@ -158,6 +160,7 @@ class DeleteMedication(APIView): """ API endpoint to delete medication if medication in database. """ + permission_classes = [IsSuperUser] @extend_schema( request=inline_serializer(name='DeleteMedicationRequest', fields={ diff --git a/server/api/views/medRules/views.py b/server/api/views/medRules/views.py index 2f80f8f3..7e4ecae5 100644 --- a/server/api/views/medRules/views.py +++ b/server/api/views/medRules/views.py @@ -1,7 +1,7 @@ from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework import status, serializers as drf_serializers +from api.permissions import IsSuperUser from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt from drf_spectacular.utils import extend_schema, inline_serializer @@ -13,7 +13,7 @@ @method_decorator(csrf_exempt, name='dispatch') class MedRules(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsSuperUser] serializer_class = MedRuleSerializer def get(self, request, format=None): diff --git a/server/api/views/text_extraction/views.py b/server/api/views/text_extraction/views.py index 020740ad..35abe976 100644 --- a/server/api/views/text_extraction/views.py +++ b/server/api/views/text_extraction/views.py @@ -3,7 +3,7 @@ import re from rest_framework.views import APIView -from rest_framework.permissions import IsAuthenticated +from api.permissions import IsSuperUser from rest_framework.response import Response from rest_framework import status from django.utils.decorators import method_decorator @@ -97,7 +97,7 @@ def anthropic_citations(client: anthropic.Client, user_prompt: str, content_chun @method_decorator(csrf_exempt, name='dispatch') class RuleExtractionAPIView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsSuperUser] @extend_schema( parameters=[ @@ -155,7 +155,7 @@ def openai_extraction(content_chunks, user_prompt): @method_decorator(csrf_exempt, name='dispatch') class RuleExtractionAPIOpenAIView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsSuperUser] @extend_schema( parameters=[ diff --git a/server/api/views/uploadFile/views.py b/server/api/views/uploadFile/views.py index eda43b76..6da092ce 100644 --- a/server/api/views/uploadFile/views.py +++ b/server/api/views/uploadFile/views.py @@ -1,5 +1,6 @@ from rest_framework.views import APIView -from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.permissions import AllowAny +from api.permissions import IsSuperUser from rest_framework.response import Response from rest_framework import status, serializers as drf_serializers from rest_framework.generics import UpdateAPIView @@ -24,7 +25,7 @@ class UploadFileView(APIView): def get_permissions(self): if self.request.method == 'GET': return [AllowAny()] # Public access - return [IsAuthenticated()] # Auth required for other methods + return [IsSuperUser()] # Superuser required for write methods def get(self, request, format=None): print("UploadFileView, get list") @@ -217,7 +218,7 @@ def get(self, request, guid, format=None): class EditFileMetadataView(UpdateAPIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsSuperUser] serializer_class = UploadFileSerializer lookup_field = 'guid' diff --git a/server/balancer_backend/settings.py b/server/balancer_backend/settings.py index a4ccaaae..fdd31c10 100644 --- a/server/balancer_backend/settings.py +++ b/server/balancer_backend/settings.py @@ -67,7 +67,7 @@ ROOT_URLCONF = "balancer_backend.urls" -CORS_ALLOW_ALL_ORIGINS = True +CORS_ALLOWED_ORIGINS = os.environ.get("CORS_ALLOWED_ORIGINS", "http://localhost:3000").split(",") TEMPLATES = [ {