From d6942383f7590b139c47e0271eafdd7fed540fd6 Mon Sep 17 00:00:00 2001 From: karastoyanov on macbookPRO Date: Tue, 2 Sep 2025 19:48:47 +0300 Subject: [PATCH 01/12] backend auth implemented --- auth/urls.py | 6 ------ auth/views.py | 3 --- skill_forge/settings.py | 18 +++++++++++++++++- skill_forge/urls.py | 2 +- {auth => userauth}/__init__.py | 0 {auth => userauth}/admin.py | 0 {auth => userauth}/apps.py | 2 +- {auth => userauth}/models.py | 0 userauth/serializers.py | 33 +++++++++++++++++++++++++++++++++ {auth => userauth}/tests.py | 0 userauth/urls.py | 12 ++++++++++++ userauth/views.py | 14 ++++++++++++++ 12 files changed, 78 insertions(+), 12 deletions(-) delete mode 100644 auth/urls.py delete mode 100644 auth/views.py rename {auth => userauth}/__init__.py (100%) rename {auth => userauth}/admin.py (100%) rename {auth => userauth}/apps.py (84%) rename {auth => userauth}/models.py (100%) create mode 100644 userauth/serializers.py rename {auth => userauth}/tests.py (100%) create mode 100644 userauth/urls.py create mode 100644 userauth/views.py diff --git a/auth/urls.py b/auth/urls.py deleted file mode 100644 index b2b9c09..0000000 --- a/auth/urls.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.urls import path - -urlpatterns = [ - path('login/', login_view.as_view(), name='login'), - path('register/', register_view.as_view(), name='register') -] \ No newline at end of file diff --git a/auth/views.py b/auth/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/auth/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/skill_forge/settings.py b/skill_forge/settings.py index c2c801a..272eacb 100644 --- a/skill_forge/settings.py +++ b/skill_forge/settings.py @@ -11,6 +11,7 @@ """ from pathlib import Path +from datetime import timedelta # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -37,10 +38,25 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'auth', 'rest_framework', + 'rest_framework_simplejwt', + 'userauth', ] +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), +} + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), + 'ROTATE_REFRESH_TOKENS': True, # Enable refresh token rotation + 'BLACKLIST_AFTER_ROTATION': True, # Blacklist old refresh tokens after rotation + 'AUTH_HEADER_TYPES': ('Bearer',), +} + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/skill_forge/urls.py b/skill_forge/urls.py index 2c8fb01..95eccb5 100644 --- a/skill_forge/urls.py +++ b/skill_forge/urls.py @@ -19,5 +19,5 @@ urlpatterns = [ path('admin/', admin.site.urls), - path('', include('auth.urls')) + path('', include('userauth.urls')) ] diff --git a/auth/__init__.py b/userauth/__init__.py similarity index 100% rename from auth/__init__.py rename to userauth/__init__.py diff --git a/auth/admin.py b/userauth/admin.py similarity index 100% rename from auth/admin.py rename to userauth/admin.py diff --git a/auth/apps.py b/userauth/apps.py similarity index 84% rename from auth/apps.py rename to userauth/apps.py index 836fe02..97dca0d 100644 --- a/auth/apps.py +++ b/userauth/apps.py @@ -3,4 +3,4 @@ class AuthConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'auth' + name = 'userauth' diff --git a/auth/models.py b/userauth/models.py similarity index 100% rename from auth/models.py rename to userauth/models.py diff --git a/userauth/serializers.py b/userauth/serializers.py new file mode 100644 index 0000000..4bbde78 --- /dev/null +++ b/userauth/serializers.py @@ -0,0 +1,33 @@ +from django.contrib.auth.models import User +from rest_framework import serializers + +class RegisterSerializer(serializers.ModelSerializer): + """ + Serializer for registering a new user. + + Validates and creates a new user using Django's built-in User model. + Password is write-only and securely hashed before saving. + """ + + password = serializers.CharField(write_only=True) + + class Meta: + model = User + fields = ('username', 'email', 'password') + + def create(self, validated_data): + """ + Create and return a new User instance, given the validated data. + + Args: + validated_data (dict): Dictionary containing username, email, and password. + + Returns: + User: A newly created User object. + """ + user = User.objects.create_user( + username=validated_data['username'], + email=validated_data['email'], + password=validated_data['password'] + ) + return user \ No newline at end of file diff --git a/auth/tests.py b/userauth/tests.py similarity index 100% rename from auth/tests.py rename to userauth/tests.py diff --git a/userauth/urls.py b/userauth/urls.py new file mode 100644 index 0000000..36a5ca0 --- /dev/null +++ b/userauth/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from .views import RegisterView +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, +) + +urlpatterns = [ + path('register/', RegisterView.as_view(), name='register'), + # path('login/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + # path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), +] \ No newline at end of file diff --git a/userauth/views.py b/userauth/views.py new file mode 100644 index 0000000..d7602c5 --- /dev/null +++ b/userauth/views.py @@ -0,0 +1,14 @@ +from rest_framework import generics +from .serializers import RegisterSerializer +from django.contrib.auth.models import User + +class RegisterView(generics.CreateAPIView): + """ + API view for user registration. + + Accepts POST requests with username, email, and password. + Returns the created user data (excluding password). + """ + + queryset = User.objects.all() + serializer_class = RegisterSerializer \ No newline at end of file From 126dd16495bd5a816ff1aae6d66272c474035012 Mon Sep 17 00:00:00 2001 From: mariqn on mint Date: Sat, 6 Sep 2025 15:55:17 +0300 Subject: [PATCH 02/12] add debug-tool-bar --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index a91ddfc..8e31876 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ asgiref==3.9.1 Django==5.2.5 +django-debug-toolbar==6.0.0 djangorestframework==3.16.1 sqlparse==0.5.3 From a56646cb5ae303c4806b0ecaa33e7ee9ee638579 Mon Sep 17 00:00:00 2001 From: mariqn on mint Date: Sat, 6 Sep 2025 16:40:27 +0300 Subject: [PATCH 03/12] appUser --- .idea/.gitignore | 3 ++ .../inspectionProfiles/profiles_settings.xml | 6 ++++ .idea/misc.xml | 7 ++++ .idea/modules.xml | 8 +++++ .idea/skill_forge_backend.iml | 15 ++++++++ .idea/vcs.xml | 6 ++++ skill_forge/settings.py | 34 +++++++++++-------- skill_forge/urls.py | 6 ++++ userauth/models.py | 11 +++++- userauth/serializers.py | 7 ++-- userauth/urls.py | 8 ++--- userauth/views.py | 5 ++- 12 files changed, 94 insertions(+), 22 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/skill_forge_backend.iml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..68b0b27 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..6bce784 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/skill_forge_backend.iml b/.idea/skill_forge_backend.iml new file mode 100644 index 0000000..175f36f --- /dev/null +++ b/.idea/skill_forge_backend.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/skill_forge/settings.py b/skill_forge/settings.py index 272eacb..db59122 100644 --- a/skill_forge/settings.py +++ b/skill_forge/settings.py @@ -38,24 +38,25 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'debug_toolbar', 'rest_framework', - 'rest_framework_simplejwt', 'userauth', ] -REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework_simplejwt.authentication.JWTAuthentication', - ), -} - -SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), - 'ROTATE_REFRESH_TOKENS': True, # Enable refresh token rotation - 'BLACKLIST_AFTER_ROTATION': True, # Blacklist old refresh tokens after rotation - 'AUTH_HEADER_TYPES': ('Bearer',), -} +AUTH_USER_MODEL = 'userauth.AppUser' +# REST_FRAMEWORK = { +# 'DEFAULT_AUTHENTICATION_CLASSES': ( +# 'rest_framework_simplejwt.authentication.JWTAuthentication', +# ), +# } +# +# SIMPLE_JWT = { +# 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), +# 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), +# 'ROTATE_REFRESH_TOKENS': True, # Enable refresh token rotation +# 'BLACKLIST_AFTER_ROTATION': True, # Blacklist old refresh tokens after rotation +# 'AUTH_HEADER_TYPES': ('Bearer',), +# } MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', @@ -65,6 +66,11 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'debug_toolbar.middleware.DebugToolbarMiddleware', +] + +INTERNAL_IPS = [ + '127.0.0.1', ] ROOT_URLCONF = 'skill_forge.urls' diff --git a/skill_forge/urls.py b/skill_forge/urls.py index 95eccb5..663bfbc 100644 --- a/skill_forge/urls.py +++ b/skill_forge/urls.py @@ -17,7 +17,13 @@ from django.contrib import admin from django.urls import path, include +from skill_forge import settings + urlpatterns = [ path('admin/', admin.site.urls), path('', include('userauth.urls')) ] + +if settings.DEBUG: + import debug_toolbar + urlpatterns = [path('__debug__/', include(debug_toolbar.urls))] + urlpatterns diff --git a/userauth/models.py b/userauth/models.py index 71a8362..c0d42f2 100644 --- a/userauth/models.py +++ b/userauth/models.py @@ -1,3 +1,12 @@ +from django.contrib.auth.models import AbstractUser from django.db import models +from django.utils.translation import gettext_lazy as _ -# Create your models here. +class AppUser(AbstractUser): + email = models.EmailField(_('email address'), unique=True) + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['username'] + +class Meta: + db_table = "app_user" \ No newline at end of file diff --git a/userauth/serializers.py b/userauth/serializers.py index 4bbde78..50ad16f 100644 --- a/userauth/serializers.py +++ b/userauth/serializers.py @@ -1,6 +1,9 @@ from django.contrib.auth.models import User from rest_framework import serializers +from userauth.models import AppUser + + class RegisterSerializer(serializers.ModelSerializer): """ Serializer for registering a new user. @@ -12,7 +15,7 @@ class RegisterSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True) class Meta: - model = User + model = AppUser fields = ('username', 'email', 'password') def create(self, validated_data): @@ -25,7 +28,7 @@ def create(self, validated_data): Returns: User: A newly created User object. """ - user = User.objects.create_user( + user = AppUser.objects.create_user( username=validated_data['username'], email=validated_data['email'], password=validated_data['password'] diff --git a/userauth/urls.py b/userauth/urls.py index 36a5ca0..4ca2ca8 100644 --- a/userauth/urls.py +++ b/userauth/urls.py @@ -1,9 +1,9 @@ from django.urls import path from .views import RegisterView -from rest_framework_simplejwt.views import ( - TokenObtainPairView, - TokenRefreshView, -) +# from rest_framework_simplejwt.views import ( +# TokenObtainPairView, +# TokenRefreshView, +# ) urlpatterns = [ path('register/', RegisterView.as_view(), name='register'), diff --git a/userauth/views.py b/userauth/views.py index d7602c5..7a8448b 100644 --- a/userauth/views.py +++ b/userauth/views.py @@ -1,4 +1,7 @@ from rest_framework import generics + +import userauth +from .models import AppUser from .serializers import RegisterSerializer from django.contrib.auth.models import User @@ -10,5 +13,5 @@ class RegisterView(generics.CreateAPIView): Returns the created user data (excluding password). """ - queryset = User.objects.all() + queryset = AppUser.objects.all() serializer_class = RegisterSerializer \ No newline at end of file From 0be88a6575c80acdbeb4934ed1207da54635d803 Mon Sep 17 00:00:00 2001 From: karastoyanov on macbookPRO Date: Sat, 6 Sep 2025 17:26:07 +0300 Subject: [PATCH 04/12] Create register functionality: - added CORS rules - register viewl & serializers --- skill_forge/settings.py | 12 ++++++++++++ userauth/models.py | 4 ++-- userauth/serializers.py | 32 ++++++++++++-------------------- userauth/views.py | 8 +++----- 4 files changed, 29 insertions(+), 27 deletions(-) diff --git a/skill_forge/settings.py b/skill_forge/settings.py index db59122..5f46a34 100644 --- a/skill_forge/settings.py +++ b/skill_forge/settings.py @@ -38,11 +38,22 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'corsheaders', 'debug_toolbar', 'rest_framework', 'userauth', ] +CORS_ALLOWED_ORIGINS = [ + 'http://localhost:5173', +] + +CORS_ALLOW_HEADERS = ['*'] + +CORS_ALLOW_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] + +CORS_ALLOW_CREDENTIALS = True + AUTH_USER_MODEL = 'userauth.AppUser' # REST_FRAMEWORK = { # 'DEFAULT_AUTHENTICATION_CLASSES': ( @@ -67,6 +78,7 @@ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware', + 'corsheaders.middleware.CorsMiddleware', ] INTERNAL_IPS = [ diff --git a/userauth/models.py b/userauth/models.py index c0d42f2..5d1f8d8 100644 --- a/userauth/models.py +++ b/userauth/models.py @@ -8,5 +8,5 @@ class AppUser(AbstractUser): USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username'] -class Meta: - db_table = "app_user" \ No newline at end of file + class Meta: + db_table = "app_user" \ No newline at end of file diff --git a/userauth/serializers.py b/userauth/serializers.py index 50ad16f..d2cabe1 100644 --- a/userauth/serializers.py +++ b/userauth/serializers.py @@ -1,10 +1,7 @@ -from django.contrib.auth.models import User from rest_framework import serializers - from userauth.models import AppUser - -class RegisterSerializer(serializers.ModelSerializer): +class AppUserSerializer(serializers.ModelSerializer): """ Serializer for registering a new user. @@ -12,25 +9,20 @@ class RegisterSerializer(serializers.ModelSerializer): Password is write-only and securely hashed before saving. """ - password = serializers.CharField(write_only=True) - + password = serializers.CharField(write_only=True, required=True) class Meta: model = AppUser - fields = ('username', 'email', 'password') - + fields = ['id', 'username', 'password', 'email', 'first_name', 'last_name', 'is_staff'] + read_only_fields = ['id', 'is_staff'] + def create(self, validated_data): - """ - Create and return a new User instance, given the validated data. - - Args: - validated_data (dict): Dictionary containing username, email, and password. - - Returns: - User: A newly created User object. - """ - user = AppUser.objects.create_user( + user = AppUser( username=validated_data['username'], email=validated_data['email'], - password=validated_data['password'] + first_name=validated_data.get('first_name', ''), + last_name=validated_data.get('last_name', ''), ) - return user \ No newline at end of file + user.set_password(validated_data['password']) + user.save() + return user + \ No newline at end of file diff --git a/userauth/views.py b/userauth/views.py index 7a8448b..2a9d98d 100644 --- a/userauth/views.py +++ b/userauth/views.py @@ -1,9 +1,7 @@ from rest_framework import generics - -import userauth from .models import AppUser -from .serializers import RegisterSerializer -from django.contrib.auth.models import User +from .serializers import AppUserSerializer + class RegisterView(generics.CreateAPIView): """ @@ -14,4 +12,4 @@ class RegisterView(generics.CreateAPIView): """ queryset = AppUser.objects.all() - serializer_class = RegisterSerializer \ No newline at end of file + serializer_class = AppUserSerializer From 99521ff061e74f36d865aca01c637ef220eb42eb Mon Sep 17 00:00:00 2001 From: karastoyanov on macbookPRO Date: Sat, 6 Sep 2025 17:31:16 +0300 Subject: [PATCH 05/12] update register url --- userauth/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/userauth/urls.py b/userauth/urls.py index 4ca2ca8..88a6cec 100644 --- a/userauth/urls.py +++ b/userauth/urls.py @@ -6,7 +6,7 @@ # ) urlpatterns = [ - path('register/', RegisterView.as_view(), name='register'), + path('register', RegisterView.as_view(), name='register'), # path('login/', TokenObtainPairView.as_view(), name='token_obtain_pair'), # path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), ] \ No newline at end of file From a2a4bff4d6e71c421f56bb17c5f751b00a4a1c66 Mon Sep 17 00:00:00 2001 From: karastoyanov on macbookPRO Date: Sat, 6 Sep 2025 17:36:19 +0300 Subject: [PATCH 06/12] removed .idea PyCharm dir --- .gitignore | 2 +- .idea/.gitignore | 3 --- .idea/inspectionProfiles/profiles_settings.xml | 6 ------ .idea/misc.xml | 7 ------- .idea/modules.xml | 8 -------- .idea/skill_forge_backend.iml | 15 --------------- .idea/vcs.xml | 6 ------ 7 files changed, 1 insertion(+), 46 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/skill_forge_backend.iml delete mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore index 0577ca4..884301c 100644 --- a/.gitignore +++ b/.gitignore @@ -174,7 +174,7 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ # Abstra # Abstra is an AI-powered process automation framework. diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 68b0b27..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 6bce784..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/skill_forge_backend.iml b/.idea/skill_forge_backend.iml deleted file mode 100644 index 175f36f..0000000 --- a/.idea/skill_forge_backend.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 9def192c13b1896d1dcb2a3cbe7c61990e936f64 Mon Sep 17 00:00:00 2001 From: karastoyanov on macbookPRO Date: Sun, 7 Sep 2025 10:56:09 +0300 Subject: [PATCH 07/12] Added test cases for user registration --- userauth/tests.py | 60 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/userauth/tests.py b/userauth/tests.py index 7ce503c..e585176 100644 --- a/userauth/tests.py +++ b/userauth/tests.py @@ -1,3 +1,59 @@ -from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APITestCase +from rest_framework import status +from .models import AppUser -# Create your tests here. +class RegistrationTestCase(APITestCase): + """ + Test suite for AppUser registration endpoint. + """ + + def setUp(self): + """ + Set up test data and endpoint URL. + """ + self.register_url = reverse('register') + self.user_data = { + 'email': 'test@example.com', + 'username': 'testuser', + 'password': 'securepassword123' + } + + def test_user_registration(self): + """ + Test successful registration of a new AppUser. + """ + response = self.client.post(self.register_url, self.user_data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(AppUser.objects.count(), 1) + self.assertEqual(AppUser.objects.get().email, 'test@example.com') + + def test_user_missing_email(self): + """ + Test registration fails when email is missing. + """ + data = self.user_data.copy() + data.pop('email') + response = self.client.post(self.register_url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn('email', response.data) + + def test_user_missing_username(self): + """ + Test registration fails when username is missing. + """ + data = self.user_data.copy() + data.pop('username') + response = self.client.post(self.register_url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn('username', response.data) + + def test_user_missing_password(self): + """ + Test registration fails when password is missing. + """ + data = self.user_data.copy() + data.pop('password') + response = self.client.post(self.register_url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn('password', response.data) \ No newline at end of file From feeba932786970f3fc7408f14c9d569e6ec1f82f Mon Sep 17 00:00:00 2001 From: mariqn on mint Date: Sun, 7 Sep 2025 19:36:04 +0300 Subject: [PATCH 08/12] create password validator --- skill_forge/settings.py | 3 +++ userauth/serializers.py | 14 +++++++++++++- userauth/validators.py | 20 ++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 userauth/validators.py diff --git a/skill_forge/settings.py b/skill_forge/settings.py index 5f46a34..c11f27a 100644 --- a/skill_forge/settings.py +++ b/skill_forge/settings.py @@ -132,6 +132,9 @@ { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, + { + "NAME": "userauth.validators.CustomPasswordValidator", + }, ] diff --git a/userauth/serializers.py b/userauth/serializers.py index d2cabe1..cc1faa8 100644 --- a/userauth/serializers.py +++ b/userauth/serializers.py @@ -1,3 +1,4 @@ +from django.contrib.auth.password_validation import validate_password from rest_framework import serializers from userauth.models import AppUser @@ -10,10 +11,21 @@ class AppUserSerializer(serializers.ModelSerializer): """ password = serializers.CharField(write_only=True, required=True) + password2 = serializers.CharField(write_only=True, required=True) + class Meta: model = AppUser - fields = ['id', 'username', 'password', 'email', 'first_name', 'last_name', 'is_staff'] + fields = ['id', 'username', 'password', 'password2', 'email', 'first_name', 'last_name', 'is_staff'] read_only_fields = ['id', 'is_staff'] + + def validate_password(self, value): + validate_password(value, self.instance) + return value + + def validate(self, attrs): + if attrs['password'] != attrs["password2"]: + raise serializers.ValidationError({"password2": "Passwords do not match."}) + return attrs def create(self, validated_data): user = AppUser( diff --git a/userauth/validators.py b/userauth/validators.py new file mode 100644 index 0000000..ea6e42e --- /dev/null +++ b/userauth/validators.py @@ -0,0 +1,20 @@ +import re +from django.core.exceptions import ValidationError +from django.utils.translation import gettext as _ + +class CustomPasswordValidator: + def validate(self, password, user=None): + + if len(password) < 8: + raise ValidationError(_("Password must be at least 8 characters long.")) + + elif not re.search(r"[A-Z]", password): + raise ValidationError(_("Password must contain at least one uppercase letter.")) + + elif not re.search(r"[a-z]", password): + raise ValidationError(_("Password must contain at least one lowercase letter.")) + + elif not re.search(r"\d", password): + raise ValidationError(_("Password must contain at least one digit.")) + + From 2aa62e0c869d6b3546a792f215fb615f911defd2 Mon Sep 17 00:00:00 2001 From: karastoyanov on macbookPRO Date: Sun, 7 Sep 2025 20:01:53 +0300 Subject: [PATCH 09/12] Update requirements for Python venv --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index 8e31876..b614963 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,8 @@ asgiref==3.9.1 Django==5.2.5 +django-cors-headers==4.7.0 django-debug-toolbar==6.0.0 djangorestframework==3.16.1 +djangorestframework_simplejwt==5.5.1 +PyJWT==2.10.1 sqlparse==0.5.3 From bf5b794941f184359514adaa3fbb6c673c788703 Mon Sep 17 00:00:00 2001 From: karastoyanov on macbookPRO Date: Sun, 7 Sep 2025 20:07:02 +0300 Subject: [PATCH 10/12] update unit tests --- userauth/tests.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/userauth/tests.py b/userauth/tests.py index e585176..91bb9b9 100644 --- a/userauth/tests.py +++ b/userauth/tests.py @@ -16,7 +16,10 @@ def setUp(self): self.user_data = { 'email': 'test@example.com', 'username': 'testuser', - 'password': 'securepassword123' + 'password': 'Password13579!!!', + 'password2': 'Password13579!!!', + 'first_name': 'Test', + 'last_name': 'User' } def test_user_registration(self): From 4703e3bd556c1f26548191218fec429269f8ad54 Mon Sep 17 00:00:00 2001 From: Aleksandar Karastoyanov Date: Sun, 7 Sep 2025 20:11:33 +0300 Subject: [PATCH 11/12] Update userauth/validators.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- userauth/validators.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/userauth/validators.py b/userauth/validators.py index ea6e42e..42bb485 100644 --- a/userauth/validators.py +++ b/userauth/validators.py @@ -5,16 +5,14 @@ class CustomPasswordValidator: def validate(self, password, user=None): + errors = [] if len(password) < 8: - raise ValidationError(_("Password must be at least 8 characters long.")) - - elif not re.search(r"[A-Z]", password): - raise ValidationError(_("Password must contain at least one uppercase letter.")) - - elif not re.search(r"[a-z]", password): - raise ValidationError(_("Password must contain at least one lowercase letter.")) - - elif not re.search(r"\d", password): - raise ValidationError(_("Password must contain at least one digit.")) - - + errors.append(_("Password must be at least 8 characters long.")) + if not re.search(r"[A-Z]", password): + errors.append(_("Password must contain at least one uppercase letter.")) + if not re.search(r"[a-z]", password): + errors.append(_("Password must contain at least one lowercase letter.")) + if not re.search(r"\d", password): + errors.append(_("Password must contain at least one digit.")) + if errors: + raise ValidationError(errors) From 04a27ef58995bd671c3cde701ff0703d920dfa99 Mon Sep 17 00:00:00 2001 From: Aleksandar Karastoyanov Date: Sun, 7 Sep 2025 20:13:31 +0300 Subject: [PATCH 12/12] Update skill_forge/settings.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- skill_forge/settings.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/skill_forge/settings.py b/skill_forge/settings.py index c11f27a..7cab9b5 100644 --- a/skill_forge/settings.py +++ b/skill_forge/settings.py @@ -48,7 +48,17 @@ 'http://localhost:5173', ] -CORS_ALLOW_HEADERS = ['*'] +CORS_ALLOW_HEADERS = [ + 'accept', + 'accept-encoding', + 'authorization', + 'content-type', + 'dnt', + 'origin', + 'user-agent', + 'x-csrftoken', + 'x-requested-with', +] CORS_ALLOW_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']