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