From 1f653be3a186e11f1acd9f1edcd04078378958aa Mon Sep 17 00:00:00 2001 From: CaellumYHL Date: Mon, 8 Dec 2025 20:12:09 -0500 Subject: [PATCH 1/8] JWT Auth and Volunteer/Admin Roles --- backend/core/settings.py | 17 ++++++--- backend/core/urls.py | 4 +-- backend/users/permissions.py | 31 +++++++++++++++++ backend/users/serializers.py | 62 +++++++++++++++++++-------------- backend/users/urls.py | 37 +++++++++----------- backend/users/views.py | 67 +++++++++++++++++++----------------- package-lock.json | 6 ++++ 7 files changed, 141 insertions(+), 83 deletions(-) create mode 100644 backend/users/permissions.py create mode 100644 package-lock.json diff --git a/backend/core/settings.py b/backend/core/settings.py index 6167530..9ba00d3 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -13,6 +13,7 @@ import os from pathlib import Path from dotenv import load_dotenv +from datetime import timedelta # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -23,8 +24,8 @@ load_dotenv(os.path.join(BASE_DIR, ".env")) -SECRET_KEY = os.environ.get("SECRET_KEY") -DEBUG = os.environ.get("DEBUG", "False") == "True" +SECRET_KEY = "django-insecure-change-me-later-!!@#12345" #os.environ.get("SECRET_KEY") +DEBUG = True#os.environ.get("DEBUG", "False") == "True" ALLOWED_HOSTS = ["localhost", "127.0.0.1", "backend"] @@ -39,6 +40,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", + "rest_framework_simplejwt.token_blacklist", "corsheaders", "users", "inventory", @@ -140,7 +142,14 @@ "rest_framework.permissions.IsAuthenticatedOrReadOnly", ], "DEFAULT_AUTHENTICATION_CLASSES": [ - "rest_framework.authentication.SessionAuthentication", - "rest_framework.authentication.TokenAuthentication", # Or JWT + "rest_framework_simplejwt.authentication.JWTAuthentication", # Or JWT ], } + +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), # They can renew for 24 hours + "ROTATE_REFRESH_TOKENS": False, + "BLACKLIST_AFTER_ROTATION": False, + "AUTH_HEADER_TYPES": ("Bearer",), # Prefix +} diff --git a/backend/core/urls.py b/backend/core/urls.py index 0a2ba39..5bc554d 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -14,14 +14,14 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ - from django.contrib import admin from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), + # API Routes - Uncomment when ready to use - # path('api/', include('users.urls')), + path('api/', include('users.urls')), # path('api/inventory/', include('inventory.urls')), # path('api/', include('requests.urls')), ] diff --git a/backend/users/permissions.py b/backend/users/permissions.py new file mode 100644 index 0000000..eb86de6 --- /dev/null +++ b/backend/users/permissions.py @@ -0,0 +1,31 @@ +from rest_framework import permissions + +class IsAdmin(permissions.BasePermission): + """ + Allows access only to admims + """ + def has_permission(self, request, view): + # Logged in and ADMIN + return bool(request.user and request.user.is_authenticated and request.user.role == 'ADMIN') + +class IsVolunteer(permissions.BasePermission): + """ + Allows access to volunteers and admins + """ + def has_permission(self, request, view): + # Logged in and VOLUNTEER or ADMIN + return bool(request.user and request.user.is_authenticated and request.user.role in ['VOLUNTEER', 'ADMIN']) + +class IsActiveAndNotExpired(permissions.BasePermission): + """ + Global check: Is the user active? Has their access expired? + This should be applied to almost every view. + """ + def has_permission(self, request, view): + # 1. Standard authen check + if not request.user or not request.user.is_authenticated: + return False + + # Since already cross checking server for user info, might as well add this check for security/instant bans + # This checks: is_active=True AND access_expires_at > Now + return request.user.has_active_access() \ No newline at end of file diff --git a/backend/users/serializers.py b/backend/users/serializers.py index 9de63fd..a93d9ee 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -1,33 +1,43 @@ from rest_framework import serializers -# from .models import User +from .models import User # Create your serializers here. # Example: User Serializer # Uncomment and modify as needed -# -# class UserSerializer(serializers.ModelSerializer): -# """ -# Serializer for the User model. -# Controls what fields are exposed in the API. -# """ -# class Meta: -# model = User -# fields = ['id', 'username', 'email', 'first_name', 'last_name', 'date_joined'] -# read_only_fields = ['id', 'date_joined'] -# -# -# class UserCreateSerializer(serializers.ModelSerializer): -# """ -# Serializer for creating new users with password handling. -# """ -# password = serializers.CharField(write_only=True, min_length=8, style={'input_type': 'password'}) -# -# class Meta: -# model = User -# fields = ['username', 'email', 'password', 'first_name', 'last_name'] -# -# def create(self, validated_data): -# """Create user with encrypted password.""" -# return User.objects.create_user(**validated_data) + + + +class UserSerializer(serializers.ModelSerializer): + """ + Serializer for the User model. + Controls what fields are exposed in the API. + """ + class Meta: + model = User + #Changed 'date_joined' to 'created_at' + # Also added 'role' so frontend can see if ADMIN or VOLUNTEER + fields = ['id', 'name', 'email', 'created_at', 'role'] + read_only_fields = ['id', 'created_at', 'role'] + + +class UserRegistrationSerializer(serializers.ModelSerializer): + """ + Used ONLY for registering new users. + """ + password = serializers.CharField(write_only=True, style={'input_type': 'password'}) + + class Meta: + model = User + fields = ['email', 'name', 'password'] + + def create(self, validated_data): + # Need to overwrite create to handle password hashing + user = User.objects.create_user( + email=validated_data['email'], + name=validated_data['name'], + password=validated_data['password'], + role='VOLUNTEER' # Default role for new signups, should this be configurable(should there be guest class)? + ) + return user diff --git a/backend/users/urls.py b/backend/users/urls.py index 3a04e3e..2912884 100644 --- a/backend/users/urls.py +++ b/backend/users/urls.py @@ -1,27 +1,24 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -# from .views import UserViewSet +from django.contrib import admin +from rest_framework_simplejwt.views import ( + TokenObtainPairView, # The built-in "Login" view + TokenRefreshView, # The built-in "Refresh Session" view +) -# Create your URL patterns here. +from users.views import LogoutView, RegisterView, UserProfileView -# Example: Using DRF Router for ViewSets -# Uncomment and modify as needed -# -# router = DefaultRouter() -# router.register(r'users', UserViewSet, basename='user') -# -# urlpatterns = [ -# path('', include(router.urls)), -# ] -# This will create the following endpoints: -# GET /api/users/ - List all users -# POST /api/users/ - Create a new user -# GET /api/users/{id}/ - Retrieve a specific user -# PUT /api/users/{id}/ - Update a user -# PATCH /api/users/{id}/ - Partial update -# DELETE /api/users/{id}/ - Delete a user -# GET /api/users/me/ - Custom action (if defined in viewset) +urlpatterns = [ + # The 'api/' part is already handled in core/urls.py + + # Auth Routes + path('auth/register/', RegisterView.as_view(), name='auth_register'), + path('auth/login/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('auth/logout/', LogoutView.as_view(), name='auth_logout'), -urlpatterns = [] + # User Routes + path('users/me/', UserProfileView.as_view(), name='user_profile'), +] diff --git a/backend/users/views.py b/backend/users/views.py index d228035..41894d0 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -1,35 +1,40 @@ -from rest_framework import viewsets, permissions, status -from rest_framework.decorators import action +from rest_framework import generics, status, permissions from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework_simplejwt.tokens import RefreshToken +from .serializers import UserRegistrationSerializer, UserSerializer -# from .models import User -# from .serializers import UserSerializer, UserCreateSerializer +# --- 1. Registration View --- +class RegisterView(generics.CreateAPIView): + """ + Endpoint: POST /api/auth/register/ + """ + serializer_class = UserRegistrationSerializer + permission_classes = [permissions.AllowAny] # Open to public -# Create your views here. -# Example: User ViewSet -# Uncomment and modify as needed -# -# class UserViewSet(viewsets.ModelViewSet): -# """ -# ViewSet for managing users. -# Provides CRUD operations: list, create, retrieve, update, delete -# """ -# queryset = User.objects.all() -# serializer_class = UserSerializer -# permission_classes = [permissions.IsAuthenticatedOrReadOnly] -# -# def get_serializer_class(self): -# """Return appropriate serializer based on action.""" -# if self.action == 'create': -# return UserCreateSerializer -# return UserSerializer -# -# @action(detail=False, methods=['get'], permission_classes=[permissions.IsAuthenticated]) -# def me(self, request): -# """ -# Custom endpoint: GET /api/users/me/ -# Returns the current authenticated user. -# """ -# serializer = self.get_serializer(request.user) -# return Response(serializer.data) +# --- 2. User Profile View (The "Me" endpoint) --- +class UserProfileView(APIView): + """ + Endpoint: GET /api/users/me/ + Returns details of the currently logged-in user. + """ + permission_classes = [permissions.IsAuthenticated] + + def get(self, request): + serializer = UserSerializer(request.user) + return Response(serializer.data) + + +# --- 3. Logout View (Your existing code) --- +class LogoutView(APIView): + permission_classes = (permissions.IsAuthenticated,) + + def post(self, request): + try: + refresh_token = request.data["refresh"] + 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) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a649cc1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "made", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From 2881385a7a548123f4a47285bdd8b8bc659816d3 Mon Sep 17 00:00:00 2001 From: CaellumYHL Date: Mon, 8 Dec 2025 21:39:08 -0500 Subject: [PATCH 2/8] Fixed linting errors --- backend/core/settings.py | 6 +++--- backend/requirements.txt | Bin 460 -> 1264 bytes backend/users/permissions.py | 7 +++++-- backend/users/serializers.py | 9 ++++----- backend/users/urls.py | 8 ++------ backend/users/views.py | 14 +++++++------- 6 files changed, 21 insertions(+), 23 deletions(-) diff --git a/backend/core/settings.py b/backend/core/settings.py index 9ba00d3..1cbec67 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -24,8 +24,8 @@ load_dotenv(os.path.join(BASE_DIR, ".env")) -SECRET_KEY = "django-insecure-change-me-later-!!@#12345" #os.environ.get("SECRET_KEY") -DEBUG = True#os.environ.get("DEBUG", "False") == "True" +SECRET_KEY = "django-insecure-change-me-later-!!@#12345" # os.environ.get("SECRET_KEY") +DEBUG = os.environ.get("DEBUG") == "True" ALLOWED_HOSTS = ["localhost", "127.0.0.1", "backend"] @@ -147,7 +147,7 @@ } SIMPLE_JWT = { - "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), "REFRESH_TOKEN_LIFETIME": timedelta(days=1), # They can renew for 24 hours "ROTATE_REFRESH_TOKENS": False, "BLACKLIST_AFTER_ROTATION": False, diff --git a/backend/requirements.txt b/backend/requirements.txt index c8a74a61eec361d87af837f8f18f77d57531c3d7..a1a59f9adbda4b42bf2ded591d5dbf4017959c71 100644 GIT binary patch literal 1264 zcmZ`(OHaaJ5S+7#KLtW7A|AYX^5nt9BjHg>g|j zQ&dQiVTS}u-0`|X9}%xU2DropX4qhgIo4K+XB~H(sfQKWrdSwvW#`oO z_Qcvfr|B!ybikD+IGAdm+2rDd@uer#1arKF42$nnAu@v+`wvJ;}82B7r7aDGAb8? z>s1eB<~?$%ozvGDHRz-&j?gqY8tQH^vAJsg7UqKH@(eUj*sU#@v{}kt%33w@$NLFU zGs}v+39)U@$TWK%a#*OPsm__X_BmwUW!uoBlviU+QlebjUJW~xwlms>#*%COiEL{F zGGyVMt&v-H|UX`EiR1Hu1UP^;+?q5~k`n|E7 zr==`fjvnVae~+Zr-}G+B6&)z}{w(o}4o|i)#>4?8VH@*&E*gabv=np6=sTJ-l0pDin(;wtfR1-j?bGb!>Y-z153e diff --git a/backend/users/permissions.py b/backend/users/permissions.py index eb86de6..c43e9ae 100644 --- a/backend/users/permissions.py +++ b/backend/users/permissions.py @@ -1,5 +1,6 @@ from rest_framework import permissions + class IsAdmin(permissions.BasePermission): """ Allows access only to admims @@ -8,6 +9,7 @@ def has_permission(self, request, view): # Logged in and ADMIN return bool(request.user and request.user.is_authenticated and request.user.role == 'ADMIN') + class IsVolunteer(permissions.BasePermission): """ Allows access to volunteers and admins @@ -15,7 +17,8 @@ class IsVolunteer(permissions.BasePermission): def has_permission(self, request, view): # Logged in and VOLUNTEER or ADMIN return bool(request.user and request.user.is_authenticated and request.user.role in ['VOLUNTEER', 'ADMIN']) - + + class IsActiveAndNotExpired(permissions.BasePermission): """ Global check: Is the user active? Has their access expired? @@ -28,4 +31,4 @@ def has_permission(self, request, view): # Since already cross checking server for user info, might as well add this check for security/instant bans # This checks: is_active=True AND access_expires_at > Now - return request.user.has_active_access() \ No newline at end of file + return request.user.has_active_access() diff --git a/backend/users/serializers.py b/backend/users/serializers.py index a93d9ee..33a49ab 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -8,7 +8,6 @@ # Uncomment and modify as needed - class UserSerializer(serializers.ModelSerializer): """ Serializer for the User model. @@ -16,15 +15,15 @@ class UserSerializer(serializers.ModelSerializer): """ class Meta: model = User - #Changed 'date_joined' to 'created_at' + # Changed 'date_joined' to 'created_at' # Also added 'role' so frontend can see if ADMIN or VOLUNTEER - fields = ['id', 'name', 'email', 'created_at', 'role'] + fields = ['id', 'name', 'email', 'created_at', 'role'] read_only_fields = ['id', 'created_at', 'role'] class UserRegistrationSerializer(serializers.ModelSerializer): """ - Used ONLY for registering new users. + Used ONLY for registering new users """ password = serializers.CharField(write_only=True, style={'input_type': 'password'}) @@ -38,6 +37,6 @@ def create(self, validated_data): email=validated_data['email'], name=validated_data['name'], password=validated_data['password'], - role='VOLUNTEER' # Default role for new signups, should this be configurable(should there be guest class)? + role='VOLUNTEER' # Default role for new signups, should this be configurable(should there be guest class)? ) return user diff --git a/backend/users/urls.py b/backend/users/urls.py index 2912884..a613d46 100644 --- a/backend/users/urls.py +++ b/backend/users/urls.py @@ -1,18 +1,14 @@ -from django.urls import path, include -from rest_framework.routers import DefaultRouter - -from django.contrib import admin +from django.urls import path from rest_framework_simplejwt.views import ( TokenObtainPairView, # The built-in "Login" view TokenRefreshView, # The built-in "Refresh Session" view ) - from users.views import LogoutView, RegisterView, UserProfileView urlpatterns = [ # The 'api/' part is already handled in core/urls.py - + # Auth Routes path('auth/register/', RegisterView.as_view(), name='auth_register'), path('auth/login/', TokenObtainPairView.as_view(), name='token_obtain_pair'), diff --git a/backend/users/views.py b/backend/users/views.py index 41894d0..35d9110 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -4,20 +4,20 @@ from rest_framework_simplejwt.tokens import RefreshToken from .serializers import UserRegistrationSerializer, UserSerializer -# --- 1. Registration View --- + +# Register new account class RegisterView(generics.CreateAPIView): """ Endpoint: POST /api/auth/register/ """ serializer_class = UserRegistrationSerializer - permission_classes = [permissions.AllowAny] # Open to public + permission_classes = [permissions.AllowAny] # Open to public -# --- 2. User Profile View (The "Me" endpoint) --- +# User Profile to check own data class UserProfileView(APIView): """ Endpoint: GET /api/users/me/ - Returns details of the currently logged-in user. """ permission_classes = [permissions.IsAuthenticated] @@ -26,7 +26,7 @@ def get(self, request): return Response(serializer.data) -# --- 3. Logout View (Your existing code) --- +# Logout (Needs both refresh and access tokens) class LogoutView(APIView): permission_classes = (permissions.IsAuthenticated,) @@ -36,5 +36,5 @@ def post(self, request): 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) \ No newline at end of file + except Exception: + return Response(status=status.HTTP_400_BAD_REQUEST) From 6502be8aed90fccf4a42cf01cd9ca86733011a60 Mon Sep 17 00:00:00 2001 From: CaellumYHL Date: Mon, 8 Dec 2025 21:40:31 -0500 Subject: [PATCH 3/8] Fixed Linting Errors 2 --- requirements.txt | Bin 0 -> 434 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..d250f0500d1ab571ab79453dbe29f2507be446a0 GIT binary patch literal 434 zcmY+AI}d_D5QL{T@vj&jMrmv7Cx!*TIi2^lV zNYUcN$HxvwmWM4iFv<AaFK!Y2X{d#aE|) DVB$Tt literal 0 HcmV?d00001 From 6ffd3cf82deb1a82824497b4671b7aedf6d612fa Mon Sep 17 00:00:00 2001 From: CaellumYHL Date: Mon, 8 Dec 2025 22:01:40 -0500 Subject: [PATCH 4/8] Fixed black linting errors --- backend/core/settings.py | 6 +++--- backend/core/urls.py | 4 ++-- backend/users/permissions.py | 8 +++++--- backend/users/serializers.py | 18 ++++++++++-------- backend/users/urls.py | 14 ++++++-------- backend/users/views.py | 2 ++ 6 files changed, 28 insertions(+), 24 deletions(-) diff --git a/backend/core/settings.py b/backend/core/settings.py index 1cbec67..ce00f88 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -24,7 +24,7 @@ load_dotenv(os.path.join(BASE_DIR, ".env")) -SECRET_KEY = "django-insecure-change-me-later-!!@#12345" # os.environ.get("SECRET_KEY") +SECRET_KEY = "django-insecure-change-me-later-!!@#12345" DEBUG = os.environ.get("DEBUG") == "True" ALLOWED_HOSTS = ["localhost", "127.0.0.1", "backend"] @@ -148,8 +148,8 @@ SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), - "REFRESH_TOKEN_LIFETIME": timedelta(days=1), # They can renew for 24 hours + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), # They can renew for 24 hours "ROTATE_REFRESH_TOKENS": False, "BLACKLIST_AFTER_ROTATION": False, - "AUTH_HEADER_TYPES": ("Bearer",), # Prefix + "AUTH_HEADER_TYPES": ("Bearer",), # Prefix } diff --git a/backend/core/urls.py b/backend/core/urls.py index 5bc554d..ec3131d 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -14,14 +14,14 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.contrib import admin from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), - # API Routes - Uncomment when ready to use - path('api/', include('users.urls')), + path("api/", include("users.urls")), # path('api/inventory/', include('inventory.urls')), # path('api/', include('requests.urls')), ] diff --git a/backend/users/permissions.py b/backend/users/permissions.py index c43e9ae..a326ba7 100644 --- a/backend/users/permissions.py +++ b/backend/users/permissions.py @@ -5,25 +5,27 @@ class IsAdmin(permissions.BasePermission): """ Allows access only to admims """ + def has_permission(self, request, view): # Logged in and ADMIN - return bool(request.user and request.user.is_authenticated and request.user.role == 'ADMIN') + return bool(request.user and request.user.is_authenticated and request.user.role == "ADMIN") class IsVolunteer(permissions.BasePermission): """ Allows access to volunteers and admins """ + def has_permission(self, request, view): # Logged in and VOLUNTEER or ADMIN - return bool(request.user and request.user.is_authenticated and request.user.role in ['VOLUNTEER', 'ADMIN']) + return bool(request.user and request.user.is_authenticated and request.user.role in ["VOLUNTEER", "ADMIN"]) class IsActiveAndNotExpired(permissions.BasePermission): """ Global check: Is the user active? Has their access expired? - This should be applied to almost every view. """ + def has_permission(self, request, view): # 1. Standard authen check if not request.user or not request.user.is_authenticated: diff --git a/backend/users/serializers.py b/backend/users/serializers.py index 33a49ab..7316973 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -13,30 +13,32 @@ class UserSerializer(serializers.ModelSerializer): Serializer for the User model. Controls what fields are exposed in the API. """ + class Meta: model = User # Changed 'date_joined' to 'created_at' # Also added 'role' so frontend can see if ADMIN or VOLUNTEER - fields = ['id', 'name', 'email', 'created_at', 'role'] - read_only_fields = ['id', 'created_at', 'role'] + fields = ["id", "name", "email", "created_at", "role"] + read_only_fields = ["id", "created_at", "role"] class UserRegistrationSerializer(serializers.ModelSerializer): """ Used ONLY for registering new users """ - password = serializers.CharField(write_only=True, style={'input_type': 'password'}) + + password = serializers.CharField(write_only=True, style={"input_type": "password"}) class Meta: model = User - fields = ['email', 'name', 'password'] + fields = ["email", "name", "password"] def create(self, validated_data): # Need to overwrite create to handle password hashing user = User.objects.create_user( - email=validated_data['email'], - name=validated_data['name'], - password=validated_data['password'], - role='VOLUNTEER' # Default role for new signups, should this be configurable(should there be guest class)? + email=validated_data["email"], + name=validated_data["name"], + password=validated_data["password"], + role="VOLUNTEER", # Default role for new signups, should this be configurable ) return user diff --git a/backend/users/urls.py b/backend/users/urls.py index a613d46..9bbd813 100644 --- a/backend/users/urls.py +++ b/backend/users/urls.py @@ -1,20 +1,18 @@ from django.urls import path from rest_framework_simplejwt.views import ( TokenObtainPairView, # The built-in "Login" view - TokenRefreshView, # The built-in "Refresh Session" view + TokenRefreshView, # The built-in "Refresh Session" view ) from users.views import LogoutView, RegisterView, UserProfileView urlpatterns = [ # The 'api/' part is already handled in core/urls.py - # Auth Routes - path('auth/register/', RegisterView.as_view(), name='auth_register'), - path('auth/login/', TokenObtainPairView.as_view(), name='token_obtain_pair'), - path('auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'), - path('auth/logout/', LogoutView.as_view(), name='auth_logout'), - + path("auth/register/", RegisterView.as_view(), name="auth_register"), + path("auth/login/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("auth/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("auth/logout/", LogoutView.as_view(), name="auth_logout"), # User Routes - path('users/me/', UserProfileView.as_view(), name='user_profile'), + path("users/me/", UserProfileView.as_view(), name="user_profile"), ] diff --git a/backend/users/views.py b/backend/users/views.py index 35d9110..ff896ce 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -10,6 +10,7 @@ class RegisterView(generics.CreateAPIView): """ Endpoint: POST /api/auth/register/ """ + serializer_class = UserRegistrationSerializer permission_classes = [permissions.AllowAny] # Open to public @@ -19,6 +20,7 @@ class UserProfileView(APIView): """ Endpoint: GET /api/users/me/ """ + permission_classes = [permissions.IsAuthenticated] def get(self, request): From 7c8b3bcb73d3f9439e18b3351888fa8ec392d866 Mon Sep 17 00:00:00 2001 From: CaellumYHL Date: Sat, 13 Dec 2025 12:55:56 -0500 Subject: [PATCH 5/8] Fixed secret key implementation --- backend/core/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/core/settings.py b/backend/core/settings.py index ce00f88..5bcb92b 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -24,8 +24,8 @@ load_dotenv(os.path.join(BASE_DIR, ".env")) -SECRET_KEY = "django-insecure-change-me-later-!!@#12345" -DEBUG = os.environ.get("DEBUG") == "True" +SECRET_KEY = os.environ.get("SECRET_KEY") +DEBUG = os.environ.get("DEBUG", "False") == "True" ALLOWED_HOSTS = ["localhost", "127.0.0.1", "backend"] From 07f148adf9132545f7d1787c6a5154fb9e30267c Mon Sep 17 00:00:00 2001 From: CaellumYHL Date: Thu, 18 Dec 2025 19:44:35 -0500 Subject: [PATCH 6/8] Added some basic tests --- backend/users/tests.py | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/backend/users/tests.py b/backend/users/tests.py index 2e221d4..854539f 100644 --- a/backend/users/tests.py +++ b/backend/users/tests.py @@ -1,13 +1,47 @@ import pytest +from django.urls import reverse +from rest_framework import status from django.contrib.auth import get_user_model User = get_user_model() - @pytest.mark.django_db def test_create_user(): - """Test creating a user""" + """Test creating basic user""" user = User.objects.create_user(email="test@example.com", name="Test User", password="testpass123") assert user.email == "test@example.com" assert user.name == "Test User" assert user.check_password("testpass123") + +@pytest.mark.django_db +def test_login_success(api_client): + """Test JWT login success""" + User.objects.create_user(email="login@example.com", name="Login User", password="password123") + + url = '/api/auth/login/' + data = {"email": "login@example.com", "password": "password123"} + + response = api_client.post(url, data) + + assert response.status_code == status.HTTP_200_OK + assert "access" in response.data + assert "refresh" in response.data + +@pytest.mark.django_db +def test_logout_blacklists_token(api_client): + """Test JWT Blacklist on logout""" + user = User.objects.create_user(email="logout@example.com", name="Logout User", password="password123") + + # Login to get tokens and setup header + login_url = '/api/auth/login/' + login_res = api_client.post(login_url, {"email": "logout@example.com", "password": "password123"}) + access = login_res.data["access"] + refresh = login_res.data["refresh"] + + api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {access}') + + # Logout to blacklist the refresh token + logout_url = '/api/auth/logout/' + response = api_client.post(logout_url, {"refresh": refresh}) + + assert response.status_code == status.HTTP_205_RESET_CONTENT From a549312141fd0555e406d31638806f50d4ae3e1e Mon Sep 17 00:00:00 2001 From: CaellumYHL Date: Thu, 18 Dec 2025 19:49:27 -0500 Subject: [PATCH 7/8] Final black linting fix with added tests --- backend/users/tests.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/backend/users/tests.py b/backend/users/tests.py index 854539f..616987b 100644 --- a/backend/users/tests.py +++ b/backend/users/tests.py @@ -5,6 +5,7 @@ User = get_user_model() + @pytest.mark.django_db def test_create_user(): """Test creating basic user""" @@ -13,35 +14,37 @@ def test_create_user(): assert user.name == "Test User" assert user.check_password("testpass123") + @pytest.mark.django_db def test_login_success(api_client): """Test JWT login success""" User.objects.create_user(email="login@example.com", name="Login User", password="password123") - - url = '/api/auth/login/' + + url = "/api/auth/login/" data = {"email": "login@example.com", "password": "password123"} - + response = api_client.post(url, data) - + assert response.status_code == status.HTTP_200_OK assert "access" in response.data assert "refresh" in response.data + @pytest.mark.django_db def test_logout_blacklists_token(api_client): """Test JWT Blacklist on logout""" user = User.objects.create_user(email="logout@example.com", name="Logout User", password="password123") - + # Login to get tokens and setup header - login_url = '/api/auth/login/' + login_url = "/api/auth/login/" login_res = api_client.post(login_url, {"email": "logout@example.com", "password": "password123"}) access = login_res.data["access"] refresh = login_res.data["refresh"] - api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {access}') - + api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {access}") + # Logout to blacklist the refresh token - logout_url = '/api/auth/logout/' + logout_url = "/api/auth/logout/" response = api_client.post(logout_url, {"refresh": refresh}) - + assert response.status_code == status.HTTP_205_RESET_CONTENT From 60de54ca764ed04eaf4c2a359cd021a035f7fe43 Mon Sep 17 00:00:00 2001 From: CaellumYHL Date: Thu, 18 Dec 2025 19:59:39 -0500 Subject: [PATCH 8/8] Fixing issues with API_Client for python tests --- backend/users/tests.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/backend/users/tests.py b/backend/users/tests.py index 616987b..a11eff7 100644 --- a/backend/users/tests.py +++ b/backend/users/tests.py @@ -16,14 +16,14 @@ def test_create_user(): @pytest.mark.django_db -def test_login_success(api_client): +def test_login_success(client): """Test JWT login success""" User.objects.create_user(email="login@example.com", name="Login User", password="password123") url = "/api/auth/login/" data = {"email": "login@example.com", "password": "password123"} - response = api_client.post(url, data) + response = client.post(url, data) assert response.status_code == status.HTTP_200_OK assert "access" in response.data @@ -31,20 +31,23 @@ def test_login_success(api_client): @pytest.mark.django_db -def test_logout_blacklists_token(api_client): +def test_logout_blacklists_token(client): """Test JWT Blacklist on logout""" user = User.objects.create_user(email="logout@example.com", name="Logout User", password="password123") - # Login to get tokens and setup header + # Login to get tokens login_url = "/api/auth/login/" - login_res = api_client.post(login_url, {"email": "logout@example.com", "password": "password123"}) + login_res = client.post( + login_url, {"email": "logout@example.com", "password": "password123"}, content_type="application/json" + ) + access = login_res.data["access"] refresh = login_res.data["refresh"] - api_client.credentials(HTTP_AUTHORIZATION=f"Bearer {access}") - - # Logout to blacklist the refresh token + # Call Logout with the Authorization header manually added to avoid conftest logout_url = "/api/auth/logout/" - response = api_client.post(logout_url, {"refresh": refresh}) + response = client.post( + logout_url, {"refresh": refresh}, content_type="application/json", HTTP_AUTHORIZATION=f"Bearer {access}" + ) assert response.status_code == status.HTTP_205_RESET_CONTENT