open the API up to the frontend
سینیور خواست زودتر به فرانتاند وصل شویم تا از آنجا هم تست شود. اینها
سمتِ ما لازم بودند:
CORS با allowlist صریح (نه ALLOW_ALL): احرازِ هویتِ ما توکنِ Bearer در هدر
است، پس origin ناشناس نباید از مرورگرِ کاربر به API ما بزند.
CORS_ALLOW_CREDENTIALS عمداً خاموش میماند — کوکیای در کار نیست و روشن
کردنش فقط سطحِ حمله اضافه میکند. CorsMiddleware بالاتر از CommonMiddleware
گذاشته شد وگرنه ریدایرکتِ APPEND_SLASH و خطای ۴۰۰ هدرِ CORS نمیگیرند و در
مرورگر بهشکلِ خطای مبهمِ شبکه دیده میشوند.
PATCH /api/user/profile اضافه شد؛ تا پیش از این هیچ راهی برای ثبتِ نامِ
کاربر نبود. GET /api/home بدونِ پارامتر حالا پایهی ذخیرهشدهی کاربر را
برمیگرداند و POST /api/ai/ask فقط با lessonId کار میکند — هر دو برای
اینکه فرانت لازم نباشد پایه را خودش نگه دارد. questionCount به
lessons/{id}/intro اضافه شد.
docs/frontend/api.ts و README.md کلاینتِ تایپدار و راهنمای اتصالاند، از
روی OpenAPI واقعیِ همین سرویس تولید شدهاند.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,27 @@ class SetGradeRequestSerializer(serializers.Serializer):
|
||||
gradeId = serializers.IntegerField()
|
||||
|
||||
|
||||
class UpdateProfileRequestSerializer(serializers.Serializer):
|
||||
"""
|
||||
ورودی PATCH /api/user/profile — همهی فیلدها اختیاریاند و فقط همانهایی
|
||||
که فرستاده شوند تغییر میکنند (partial update واقعی).
|
||||
|
||||
`phone` و `isGuest` عمداً اینجا نیستند: شماره فقط از مسیرِ OTP عوض
|
||||
میشود و مهمانبودن را کاربر نباید بتواند خودش تغییر دهد.
|
||||
"""
|
||||
name = serializers.CharField(max_length=100, required=False, allow_blank=True)
|
||||
avatar = serializers.CharField(max_length=100, required=False, allow_blank=True)
|
||||
birthYear = serializers.IntegerField(required=False, allow_null=True,
|
||||
min_value=1300, max_value=1500)
|
||||
parentPhone = serializers.CharField(max_length=15, required=False, allow_blank=True)
|
||||
bio = serializers.CharField(max_length=200, required=False, allow_blank=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
if not attrs:
|
||||
raise serializers.ValidationError('حداقل یکی از فیلدها باید ارسال شود.')
|
||||
return attrs
|
||||
|
||||
|
||||
class RequestOTPRequestSerializer(serializers.Serializer):
|
||||
"""اپ کمکی برای گرفتن OTP (در سند اصلی نبود، برای تستپذیری Swagger اضافه شد)."""
|
||||
phone = serializers.CharField(max_length=15)
|
||||
|
||||
@@ -28,6 +28,7 @@ from .serializers import (
|
||||
RequestOTPRequestSerializer,
|
||||
RequestOTPResponseSerializer,
|
||||
SetGradeRequestSerializer,
|
||||
UpdateProfileRequestSerializer,
|
||||
SetGradeResponseSerializer,
|
||||
)
|
||||
|
||||
@@ -379,9 +380,25 @@ class SetUserGradeView(APIView):
|
||||
|
||||
|
||||
class UserProfileView(APIView):
|
||||
"""GET /api/user/profile — اطلاعات پروفایل کاربر جاری (نیاز به احراز هویت)"""
|
||||
"""
|
||||
GET /api/user/profile — اطلاعات پروفایل کاربر جاری
|
||||
PATCH /api/user/profile — ویرایشِ همان اطلاعات (نیاز به احراز هویت)
|
||||
|
||||
PATCH برای صفحهی «بیا باهم آشنا بشیم» در اپ لازم است: آنجا اسم (و بعداً
|
||||
آواتار) گرفته میشود و تا پیش از این هیچ مسیری برای نوشتنشان وجود نداشت —
|
||||
فقط پایهی تحصیلی endpoint اختصاصیِ خودش را داشت.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
# نوشتن است، پس همان throttleِ نوشتنِ پروفایل که SetUserGradeView دارد.
|
||||
throttle_scope = 'user_profile_write'
|
||||
throttle_classes = [GuestAwareAccountThrottle]
|
||||
|
||||
def get_throttles(self):
|
||||
# GET خواندنی است و نباید سهمیهی نوشتن را مصرف کند.
|
||||
if self.request.method == 'GET':
|
||||
return []
|
||||
return super().get_throttles()
|
||||
|
||||
@extend_schema(
|
||||
responses=ProfileResponseSerializer,
|
||||
@@ -407,3 +424,49 @@ class UserProfileView(APIView):
|
||||
'bio': profile.bio,
|
||||
}
|
||||
return Response(ProfileResponseSerializer(data).data)
|
||||
|
||||
@extend_schema(
|
||||
request=UpdateProfileRequestSerializer,
|
||||
responses=ProfileResponseSerializer,
|
||||
tags=['Identity & Profile (accounts)'],
|
||||
)
|
||||
def patch(self, request):
|
||||
serializer = UpdateProfileRequestSerializer(data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
v = serializer.validated_data
|
||||
|
||||
user = request.user
|
||||
profile, _ = Profile.objects.get_or_create(user_id=user.id)
|
||||
|
||||
user_fields = {'name': 'name', 'avatar': 'avatar'}
|
||||
profile_fields = {'birthYear': 'birth_year', 'parentPhone': 'parent_phone', 'bio': 'bio'}
|
||||
|
||||
changed_user = [attr for key, attr in user_fields.items() if key in v]
|
||||
changed_profile = [attr for key, attr in profile_fields.items() if key in v]
|
||||
|
||||
for key, attr in user_fields.items():
|
||||
if key in v:
|
||||
setattr(user, attr, v[key])
|
||||
for key, attr in profile_fields.items():
|
||||
if key in v:
|
||||
setattr(profile, attr, v[key])
|
||||
|
||||
# update_fields: فقط همان ستونهایی که واقعاً عوض شدهاند نوشته شوند تا
|
||||
# PATCH همزمان از یک دستگاهِ دیگر، فیلدهای دستنخورده را بازنویسی نکند.
|
||||
if changed_user:
|
||||
user.save(update_fields=changed_user)
|
||||
if changed_profile:
|
||||
profile.save(update_fields=changed_profile)
|
||||
|
||||
data = {
|
||||
'userId': str(user.id),
|
||||
'phone': user.phone,
|
||||
'name': user.name,
|
||||
'avatar': user.avatar,
|
||||
'isGuest': user.is_guest,
|
||||
'gradeId': user.grade_id,
|
||||
'birthYear': profile.birth_year,
|
||||
'parentPhone': profile.parent_phone,
|
||||
'bio': profile.bio,
|
||||
}
|
||||
return Response(ProfileResponseSerializer(data).data)
|
||||
|
||||
Reference in New Issue
Block a user