137 lines
5.1 KiB
Python
137 lines
5.1 KiB
Python
from typing import Any
|
|
|
|
import re
|
|
|
|
from django.contrib.auth import authenticate
|
|
from django.contrib.auth.hashers import make_password
|
|
from django.contrib.auth.password_validation import validate_password
|
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
|
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 SanitizedCharField(serializers.CharField):
|
|
"""CharField that strips control characters on deserialization."""
|
|
|
|
def to_internal_value(self, data: object) -> object:
|
|
value = super().to_internal_value(data)
|
|
if isinstance(value, str):
|
|
value = value.strip()
|
|
if "\x00" in value:
|
|
raise serializers.ValidationError("Input contains invalid characters.")
|
|
value = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", value)
|
|
return value
|
|
|
|
|
|
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 = SanitizedCharField(max_length=150, required=False, allow_blank=True, default="")
|
|
last_name = SanitizedCharField(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 and Django validators."""
|
|
if len(value) < MIN_PASSWORD_LENGTH:
|
|
raise serializers.ValidationError(
|
|
f"Password must be at least {MIN_PASSWORD_LENGTH} characters."
|
|
)
|
|
try:
|
|
validate_password(value)
|
|
except DjangoValidationError as e:
|
|
raise serializers.ValidationError(" ".join(e.messages))
|
|
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"])
|
|
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 — limited fields, no email."""
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = ("id", "first_name", "last_name")
|
|
|
|
|
|
class RefreshRequestSerializer(serializers.Serializer):
|
|
"""Validate a refresh token request."""
|
|
|
|
refresh = serializers.CharField(required=True)
|
|
|
|
def validate_refresh(self, value: str) -> str:
|
|
if not value or not value.strip():
|
|
raise serializers.ValidationError("Refresh token is required.")
|
|
return value.strip()
|
|
|
|
|
|
class LogoutSerializer(serializers.Serializer):
|
|
"""Validate a logout request."""
|
|
|
|
refresh = serializers.CharField(required=True)
|
|
|
|
def validate_refresh(self, value: str) -> str:
|
|
if not value or not value.strip():
|
|
raise serializers.ValidationError("Refresh token is required.")
|
|
return value.strip() |