174 lines
6.9 KiB
Python
174 lines
6.9 KiB
Python
from rest_framework import status
|
||
from adrf.views import APIView
|
||
from rest_framework.permissions import IsAuthenticated
|
||
from asgiref.sync import sync_to_async
|
||
from drf_yasg.utils import swagger_auto_schema
|
||
from drf_yasg import openapi
|
||
import logging
|
||
from chunyu_project.common_schemas import success_response, error_response, unauthorized_response, not_found_response
|
||
|
||
from utils.response_codes import (
|
||
ResponseCode,
|
||
create_standardized_response,
|
||
create_standardized_error_response
|
||
)
|
||
from utils.email_utils import validate_email_mx
|
||
from ..serializers.user_serializers import (
|
||
SendEmailCodeSerializer,
|
||
ChangeEmailSerializer,
|
||
UserSerializer,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class SendChangeEmailCodeAPIView(APIView):
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@swagger_auto_schema(
|
||
tags=['用户'],
|
||
operation_summary='发送修改邮箱验证码',
|
||
operation_description='向新邮箱发送验证码用于修改邮箱',
|
||
request_body=openapi.Schema(
|
||
type=openapi.TYPE_OBJECT,
|
||
properties={
|
||
'email': openapi.Schema(type=openapi.TYPE_STRING, description='新邮箱地址'),
|
||
'captcha_key': openapi.Schema(type=openapi.TYPE_STRING, description='图形验证码 key'),
|
||
'captcha_code': openapi.Schema(type=openapi.TYPE_STRING, description='图形验证码'),
|
||
},
|
||
),
|
||
responses={200: success_response, 400: error_response, 401: unauthorized_response, 500: error_response},
|
||
)
|
||
async def post(self, request):
|
||
from utils.captcha import check_captcha_required, verify_captcha, record_failure, reset_failures
|
||
|
||
identifier = str(request.user.id)
|
||
operation = 'change_email'
|
||
captcha_required = await sync_to_async(check_captcha_required)(operation, identifier)
|
||
|
||
if captcha_required:
|
||
captcha_key = request.data.get('captcha_key', None)
|
||
captcha_code = request.data.get('captcha_code', None)
|
||
|
||
if not captcha_key or not captcha_code:
|
||
return create_standardized_error_response(
|
||
code=ResponseCode.CAPTCHA_REQUIRED,
|
||
status_code=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
captcha_result = await sync_to_async(verify_captcha)(captcha_key, captcha_code)
|
||
if captcha_result == 'expired':
|
||
return create_standardized_error_response(
|
||
code=ResponseCode.CAPTCHA_EXPIRED,
|
||
status_code=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
elif captcha_result == 'wrong':
|
||
await sync_to_async(record_failure)(operation, identifier)
|
||
return create_standardized_error_response(
|
||
code=ResponseCode.CAPTCHA_ERROR,
|
||
status_code=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
serializer = SendEmailCodeSerializer(
|
||
data=request.data,
|
||
context={'request': request}
|
||
)
|
||
|
||
# 校验器内含同步 ORM(validate_email 查重)与 cache 写入,线程池兜底
|
||
if not await sync_to_async(serializer.is_valid)():
|
||
errors = serializer.errors
|
||
first_error = ''
|
||
for field, msgs in errors.items():
|
||
if isinstance(msgs, list) and msgs:
|
||
first_error = str(msgs[0])
|
||
break
|
||
|
||
await sync_to_async(record_failure)(operation, identifier)
|
||
return create_standardized_error_response(
|
||
data=errors,
|
||
code=ResponseCode.PARAMETER_ERROR,
|
||
message=first_error or '参数异常',
|
||
status_code=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
email = serializer.validated_data.get('email')
|
||
if email and not await sync_to_async(validate_email_mx)(email):
|
||
logger.warning(f'[ChangeEmail] Domain MX check failed: email={email}')
|
||
await sync_to_async(record_failure)(operation, identifier)
|
||
return create_standardized_error_response(
|
||
code=ResponseCode.EMAIL_DOMAIN_INVALID,
|
||
status_code=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
try:
|
||
# save() 内部触发验证码邮件发送(Celery/cache/SMTP 链路),线程池兜底
|
||
email = await serializer.asave()
|
||
await sync_to_async(reset_failures)(operation, identifier)
|
||
return create_standardized_response(
|
||
data={'email_sent': True},
|
||
code=ResponseCode.EMAIL_CHANGE_CODE_SENT,
|
||
status_code=status.HTTP_200_OK
|
||
)
|
||
except Exception as e:
|
||
await sync_to_async(record_failure)(operation, identifier)
|
||
return create_standardized_error_response(
|
||
message=f'邮件发送失败: {str(e)}',
|
||
code=ResponseCode.EMAIL_SEND_FAILED,
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
|
||
|
||
class ChangeEmailAPIView(APIView):
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@swagger_auto_schema(
|
||
tags=['用户'],
|
||
operation_summary='修改邮箱',
|
||
operation_description='使用邮箱验证码修改用户邮箱',
|
||
request_body=openapi.Schema(
|
||
type=openapi.TYPE_OBJECT,
|
||
properties={
|
||
'email': openapi.Schema(type=openapi.TYPE_STRING, description='新邮箱地址'),
|
||
'email_code': openapi.Schema(type=openapi.TYPE_STRING, description='邮箱验证码'),
|
||
},
|
||
),
|
||
responses={200: success_response, 400: error_response, 401: unauthorized_response, 500: error_response},
|
||
)
|
||
async def post(self, request):
|
||
serializer = ChangeEmailSerializer(
|
||
data=request.data,
|
||
context={'request': request}
|
||
)
|
||
|
||
# 校验器内含同步 ORM 查询,线程池兜底
|
||
if not await sync_to_async(serializer.is_valid)():
|
||
errors = serializer.errors
|
||
first_error = ''
|
||
for field, msgs in errors.items():
|
||
if isinstance(msgs, list) and msgs:
|
||
first_error = str(msgs[0])
|
||
break
|
||
|
||
return create_standardized_error_response(
|
||
data=errors,
|
||
code=ResponseCode.PARAMETER_ERROR,
|
||
message=first_error or '参数异常',
|
||
status_code=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
try:
|
||
updated_user = await serializer.asave()
|
||
user_serializer = UserSerializer(updated_user)
|
||
|
||
return create_standardized_response(
|
||
data={'user': await user_serializer.adata},
|
||
code=ResponseCode.EMAIL_CHANGED,
|
||
status_code=status.HTTP_200_OK
|
||
)
|
||
except Exception as e:
|
||
return create_standardized_error_response(
|
||
message=f'邮箱修改失败: {str(e)}',
|
||
code=ResponseCode.SERVER_INTERNAL_ERROR,
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|