mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 06:51:08 -05:00
- Centralize API endpoints in dedicated api app with v1 versioning - Remove individual API modules from parks and rides apps - Add event tracking system with analytics functionality - Integrate Vue.js frontend with Tailwind CSS v4 and TypeScript - Add comprehensive database migrations for event tracking - Implement user authentication and social provider setup - Add API schema documentation and serializers - Configure development environment with shared scripts - Update project structure for monorepo with frontend/backend separation
247 lines
7.2 KiB
Python
247 lines
7.2 KiB
Python
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()
|