Files
job-tracker/api/accounts/serializers.py
Marko (Hermes Implementer) 6867a91b67 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
2026-05-26 00:54:27 +00:00

96 lines
3.7 KiB
Python

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")