feat: major API restructure and Vue.js frontend integration

- 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
This commit is contained in:
pacnpal
2025-08-24 16:42:20 -04:00
parent 92f4104d7a
commit e62646bcf9
127 changed files with 27734 additions and 1867 deletions

View File

@@ -0,0 +1,246 @@
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()