Skip to content
Merged
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 0 additions & 3 deletions auth/models.py

This file was deleted.

3 changes: 0 additions & 3 deletions auth/tests.py

This file was deleted.

6 changes: 0 additions & 6 deletions auth/urls.py

This file was deleted.

3 changes: 0 additions & 3 deletions auth/views.py

This file was deleted.

4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +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
49 changes: 48 additions & 1 deletion skill_forge/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,10 +38,47 @@
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'auth',
'corsheaders',
'debug_toolbar',
'rest_framework',
'userauth',
]

CORS_ALLOWED_ORIGINS = [
'http://localhost:5173',
]

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']

CORS_ALLOW_CREDENTIALS = True

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',
'django.contrib.sessions.middleware.SessionMiddleware',
Expand All @@ -49,6 +87,12 @@
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',
'corsheaders.middleware.CorsMiddleware',
]

INTERNAL_IPS = [
'127.0.0.1',
]

ROOT_URLCONF = 'skill_forge.urls'
Expand Down Expand Up @@ -98,6 +142,9 @@
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
{
"NAME": "userauth.validators.CustomPasswordValidator",
},
]


Expand Down
8 changes: 7 additions & 1 deletion skill_forge/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('auth.urls'))
path('', include('userauth.urls'))
]

if settings.DEBUG:
import debug_toolbar
urlpatterns = [path('__debug__/', include(debug_toolbar.urls))] + urlpatterns
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion auth/apps.py → userauth/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@

class AuthConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'auth'
name = 'userauth'
12 changes: 12 additions & 0 deletions userauth/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _

class AppUser(AbstractUser):
email = models.EmailField(_('email address'), unique=True)

USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']

class Meta:
db_table = "app_user"
40 changes: 40 additions & 0 deletions userauth/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from django.contrib.auth.password_validation import validate_password
from rest_framework import serializers
from userauth.models import AppUser

class AppUserSerializer(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, required=True)
password2 = serializers.CharField(write_only=True, required=True)

class Meta:
model = AppUser
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(
username=validated_data['username'],
email=validated_data['email'],
first_name=validated_data.get('first_name', ''),
last_name=validated_data.get('last_name', ''),
)
user.set_password(validated_data['password'])
user.save()
return user

62 changes: 62 additions & 0 deletions userauth/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status
from .models import AppUser

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': 'Password13579!!!',
'password2': 'Password13579!!!',
'first_name': 'Test',
'last_name': 'User'
}

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)
12 changes: 12 additions & 0 deletions userauth/urls.py
Original file line number Diff line number Diff line change
@@ -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'),
]
18 changes: 18 additions & 0 deletions userauth/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import re
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _

class CustomPasswordValidator:
def validate(self, password, user=None):

errors = []
if len(password) < 8:
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)
15 changes: 15 additions & 0 deletions userauth/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from rest_framework import generics
from .models import AppUser
from .serializers import AppUserSerializer


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 = AppUser.objects.all()
serializer_class = AppUserSerializer
Loading