feat: add user authentication with separate login and register pages
- Custom User model with email as unique identifier (AUTH_USER_MODEL) - POST /api/auth/register/ with email validation, password min 8 chars, duplicate rejection - POST /api/auth/login/ returning JWT (access + refresh) tokens - Passwords hashed via Django's make_password - React LoginPage and RegisterPage with form validation - AuthContext with useReducer for auth state management - Axios API client with JWT token injection - TypeScript conversion of frontend scaffold
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import re
|
||||
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from accounts.models import User
|
||||
|
||||
EMAIL_REGEX: re.Pattern[str] = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
|
||||
MIN_PASSWORD_LENGTH: int = 8
|
||||
|
||||
|
||||
class RegisterSerializer(serializers.Serializer):
|
||||
"""Validate and create a new user account."""
|
||||
|
||||
email = serializers.EmailField(max_length=254)
|
||||
password = serializers.CharField(write_only=True)
|
||||
password_confirm = serializers.CharField(write_only=True)
|
||||
first_name = serializers.CharField(max_length=150, required=False, allow_blank=True, default="")
|
||||
last_name = serializers.CharField(max_length=150, required=False, allow_blank=True, default="")
|
||||
|
||||
def validate_email(self, value: str) -> str:
|
||||
"""Validate email format and check for duplicates."""
|
||||
if not EMAIL_REGEX.match(value):
|
||||
raise serializers.ValidationError("Invalid email format.")
|
||||
if User.objects.filter(email__iexact=value).exists():
|
||||
raise serializers.ValidationError("A user with this email already exists.")
|
||||
return value.lower()
|
||||
|
||||
def validate_password(self, value: str) -> str:
|
||||
"""Enforce minimum password length."""
|
||||
if len(value) < MIN_PASSWORD_LENGTH:
|
||||
raise serializers.ValidationError(
|
||||
f"Password must be at least {MIN_PASSWORD_LENGTH} characters."
|
||||
)
|
||||
return value
|
||||
|
||||
def validate(self, attrs: dict[str, object]) -> dict[str, object]:
|
||||
"""Ensure password and confirmation match."""
|
||||
password = attrs.get("password")
|
||||
password_confirm = attrs.get("password_confirm")
|
||||
if password and password_confirm and password != password_confirm:
|
||||
raise serializers.ValidationError(
|
||||
{"password_confirm": "Passwords do not match."}
|
||||
)
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data: dict[str, object]) -> User:
|
||||
"""Create and return the new user."""
|
||||
validated_data.pop("password_confirm")
|
||||
validated_data["password"] = make_password(validated_data["password"])
|
||||
# Ensure username is blank rather than None for unique constraint
|
||||
validated_data.setdefault("username", "")
|
||||
return User.objects.create(**validated_data)
|
||||
|
||||
|
||||
class LoginSerializer(serializers.Serializer):
|
||||
"""Authenticate user credentials and return JWT tokens."""
|
||||
|
||||
email = serializers.EmailField(max_length=254)
|
||||
password = serializers.CharField(write_only=True)
|
||||
|
||||
def validate(self, attrs: dict[str, object]) -> dict[str, object]:
|
||||
"""Authenticate the user and generate tokens."""
|
||||
email = attrs.get("email", "")
|
||||
password = attrs.get("password", "")
|
||||
|
||||
if not email or not password:
|
||||
raise serializers.ValidationError("Both email and password are required.")
|
||||
|
||||
user = authenticate(
|
||||
request=self.context.get("request"),
|
||||
username=email,
|
||||
password=password,
|
||||
)
|
||||
if user is None:
|
||||
raise serializers.ValidationError("Invalid email or password.")
|
||||
|
||||
if not user.is_active:
|
||||
raise serializers.ValidationError("This account is inactive.")
|
||||
|
||||
refresh = RefreshToken.for_user(user)
|
||||
attrs["user"] = user
|
||||
attrs["access"] = str(refresh.access_token)
|
||||
attrs["refresh"] = str(refresh)
|
||||
return attrs
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""Public user profile serializer."""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("id", "email", "first_name", "last_name")
|
||||
Reference in New Issue
Block a user