feat: Implement avatar upload system with Cloudflare integration

- Added migration to transition avatar data from CloudflareImageField to ForeignKey structure in UserProfile.
- Fixed UserProfileEvent avatar field to align with new avatar structure.
- Created serializers for social authentication, including connected and available providers.
- Developed request logging middleware for comprehensive request/response logging.
- Updated moderation and parks migrations to remove outdated triggers and adjust foreign key relationships.
- Enhanced rides migrations to ensure proper handling of image uploads and triggers.
- Introduced a test script for the 3-step avatar upload process, ensuring functionality with Cloudflare.
- Documented the fix for avatar upload issues, detailing root cause, implementation, and verification steps.
- Implemented automatic deletion of Cloudflare images upon avatar, park, and ride photo changes or removals.
This commit is contained in:
pacnpal
2025-08-30 21:20:25 -04:00
parent fb6726f89a
commit 9bed782784
75 changed files with 4571 additions and 1962 deletions

View File

@@ -96,8 +96,8 @@ class UserOutputSerializer(serializers.ModelSerializer):
@extend_schema_field(serializers.URLField(allow_null=True))
def get_avatar_url(self, obj) -> str | None:
"""Get user avatar URL."""
if hasattr(obj, "profile") and obj.profile.avatar:
return obj.profile.avatar.url
if hasattr(obj, "profile") and obj.profile:
return obj.profile.get_avatar_url()
return None
@@ -185,25 +185,92 @@ class SignupInputSerializer(serializers.ModelSerializer):
return attrs
def create(self, validated_data):
"""Create user with validated data."""
"""Create user with validated data and send verification email."""
validated_data.pop("password_confirm", None)
password = validated_data.pop("password")
# Use type: ignore for Django's create_user method which isn't properly typed
# Create inactive user - they need to verify email first
user = UserModel.objects.create_user( # type: ignore[attr-defined]
password=password, **validated_data
password=password, is_active=False, **validated_data
)
# Create email verification record and send email
self._send_verification_email(user)
return user
def _send_verification_email(self, user):
"""Send email verification to the user."""
from apps.accounts.models import EmailVerification
from django.utils.crypto import get_random_string
from django_forwardemail.services import EmailService
from django.contrib.sites.shortcuts import get_current_site
import logging
logger = logging.getLogger(__name__)
# Create or update email verification record
verification, created = EmailVerification.objects.get_or_create(
user=user,
defaults={'token': get_random_string(64)}
)
if not created:
# Update existing token and timestamp
verification.token = get_random_string(64)
verification.save()
# Get current site from request context
request = self.context.get('request')
if request:
site = get_current_site(request._request)
# Build verification URL
verification_url = request.build_absolute_uri(
f"/api/v1/auth/verify-email/{verification.token}/"
)
# Send verification email
try:
response = EmailService.send_email(
to=user.email,
subject="Verify your ThrillWiki account",
text=f"""
Welcome to ThrillWiki!
Please verify your email address by clicking the link below:
{verification_url}
If you didn't create an account, you can safely ignore this email.
Thanks,
The ThrillWiki Team
""".strip(),
site=site,
)
# Log the ForwardEmail email ID from the response
email_id = response.get('id') if response else None
if email_id:
logger.info(
f"Verification email sent successfully to {user.email}. ForwardEmail ID: {email_id}")
else:
logger.info(
f"Verification email sent successfully to {user.email}. No email ID in response.")
except Exception as e:
# Log the error but don't fail registration
logger.error(f"Failed to send verification email to {user.email}: {e}")
class SignupOutputSerializer(serializers.Serializer):
"""Output serializer for successful signup."""
access = serializers.CharField()
refresh = serializers.CharField()
access = serializers.CharField(allow_null=True)
refresh = serializers.CharField(allow_null=True)
user = UserOutputSerializer()
message = serializers.CharField()
email_verification_required = serializers.BooleanField(default=False)
class PasswordResetInputSerializer(serializers.Serializer):
@@ -375,7 +442,7 @@ class UserProfileOutputSerializer(serializers.Serializer):
@extend_schema_field(serializers.URLField(allow_null=True))
def get_avatar_url(self, obj) -> str | None:
return obj.get_avatar()
return obj.get_avatar_url()
@extend_schema_field(serializers.DictField())
def get_user(self, obj) -> Dict[str, Any]: