from rest_framework import serializers from django.contrib.auth import get_user_model from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ValidationError from django.utils.crypto import get_random_string from django.utils import timezone from datetime import timedelta from django.contrib.sites.shortcuts import get_current_site from .models import User, PasswordReset from apps.email_service.services import EmailService from django.template.loader import render_to_string UserModel = get_user_model() class UserSerializer(serializers.ModelSerializer): """ User serializer for API responses """ avatar_url = serializers.SerializerMethodField() class Meta: model = User fields = [ 'id', 'username', 'email', 'first_name', 'last_name', 'date_joined', 'is_active', 'avatar_url' ] read_only_fields = ['id', 'date_joined', 'is_active'] def get_avatar_url(self, obj): """Get user avatar URL""" if hasattr(obj, 'profile') and obj.profile.avatar: return obj.profile.avatar.url return None class LoginSerializer(serializers.Serializer): """ Serializer for user login """ username = serializers.CharField( max_length=254, help_text="Username or email address" ) password = serializers.CharField( max_length=128, style={'input_type': 'password'}, trim_whitespace=False ) def validate(self, attrs): username = attrs.get('username') password = attrs.get('password') if username and password: return attrs raise serializers.ValidationError( 'Must include username/email and password.' ) class SignupSerializer(serializers.ModelSerializer): """ Serializer for user registration """ password = serializers.CharField( write_only=True, validators=[validate_password], style={'input_type': 'password'} ) password_confirm = serializers.CharField( write_only=True, style={'input_type': 'password'} ) class Meta: model = User fields = [ 'username', 'email', 'first_name', 'last_name', 'password', 'password_confirm' ] extra_kwargs = { 'password': {'write_only': True}, 'email': {'required': True}, } def validate_email(self, value): """Validate email is unique""" if UserModel.objects.filter(email=value).exists(): raise serializers.ValidationError( "A user with this email already exists." ) return value def validate_username(self, value): """Validate username is unique""" if UserModel.objects.filter(username=value).exists(): raise serializers.ValidationError( "A user with this username already exists." ) return value def validate(self, attrs): """Validate passwords match""" password = attrs.get('password') password_confirm = attrs.get('password_confirm') if password != password_confirm: raise serializers.ValidationError({ 'password_confirm': 'Passwords do not match.' }) return attrs def create(self, validated_data): """Create user with validated data""" validated_data.pop('password_confirm', None) password = validated_data.pop('password') user = UserModel.objects.create( **validated_data ) user.set_password(password) user.save() return user class PasswordResetSerializer(serializers.Serializer): """ Serializer for password reset request """ email = serializers.EmailField() def validate_email(self, value): """Validate email exists""" try: user = UserModel.objects.get(email=value) self.user = user return value except UserModel.DoesNotExist: # Don't reveal if email exists or not for security return value def save(self, **kwargs): """Send password reset email if user exists""" if hasattr(self, 'user'): # Create password reset token token = get_random_string(64) PasswordReset.objects.update_or_create( user=self.user, defaults={ 'token': token, 'expires_at': timezone.now() + timedelta(hours=24), 'used': False } ) # Send reset email request = self.context.get('request') if request: site = get_current_site(request) reset_url = f"{request.scheme}://{site.domain}/reset-password/{token}/" context = { 'user': self.user, 'reset_url': reset_url, 'site_name': site.name, } email_html = render_to_string( 'accounts/email/password_reset.html', context ) EmailService.send_email( to=getattr(self.user, 'email', None), subject="Reset your password", text=f"Click the link to reset your password: {reset_url}", site=site, html=email_html, ) class PasswordChangeSerializer(serializers.Serializer): """ Serializer for password change """ old_password = serializers.CharField( max_length=128, style={'input_type': 'password'} ) new_password = serializers.CharField( max_length=128, validators=[validate_password], style={'input_type': 'password'} ) new_password_confirm = serializers.CharField( max_length=128, style={'input_type': 'password'} ) def validate_old_password(self, value): """Validate old password is correct""" user = self.context['request'].user if not user.check_password(value): raise serializers.ValidationError( 'Old password is incorrect.' ) return value def validate(self, attrs): """Validate new passwords match""" new_password = attrs.get('new_password') new_password_confirm = attrs.get('new_password_confirm') if new_password != new_password_confirm: raise serializers.ValidationError({ 'new_password_confirm': 'New passwords do not match.' }) return attrs def save(self, **kwargs): """Change user password""" user = self.context['request'].user new_password = self.initial_data.get( 'new_password') if self.initial_data else None if new_password is None: raise serializers.ValidationError('New password is required.') user.set_password(new_password) user.save() return user class SocialProviderSerializer(serializers.Serializer): """ Serializer for social authentication providers """ id = serializers.CharField() name = serializers.CharField() login_url = serializers.URLField()