diff --git a/offer_backend/apps/accounts/serializers.py b/offer_backend/apps/accounts/serializers.py index 9112c6b..d672208 100644 --- a/offer_backend/apps/accounts/serializers.py +++ b/offer_backend/apps/accounts/serializers.py @@ -6,11 +6,11 @@ User = get_user_model() class RegisterSerializer(serializers.Serializer): - """邮箱验证码注册 serializer""" + """密码注册 serializer""" username = serializers.CharField(max_length=150) email = serializers.EmailField() phone = serializers.CharField(max_length=20) - code = serializers.CharField(max_length=6, min_length=6) + password = serializers.CharField(write_only=True, min_length=6) def validate_username(self, value): """验证用户名是否已存在""" @@ -24,39 +24,15 @@ class RegisterSerializer(serializers.Serializer): raise serializers.ValidationError('邮箱已被注册') return value - def validate(self, attrs): - """验证邮箱和验证码""" - email = attrs.get('email') - code = attrs.get('code') - - # 检查验证码 - try: - vc = VerificationCode.objects.filter(email=email).latest('created_at') - except VerificationCode.DoesNotExist: - raise serializers.ValidationError({'code': '请先获取验证码'}) - - # 检查验证码是否有效 - if not vc.is_valid(): - raise serializers.ValidationError({'code': '验证码已过期或已使用'}) - - # 验证码是否正确 - if vc.code != code: - vc.increment_attempts() - raise serializers.ValidationError({'code': '验证码错误'}) - - attrs['vc'] = vc - return attrs - def create(self, validated_data): - """创建用户并标记验证码为已使用""" - vc = validated_data.pop('vc') + """创建用户""" user = User.objects.create_user( username=validated_data['username'], email=validated_data['email'], phone=validated_data['phone'], + password=validated_data['password'], role='seeker' ) - vc.mark_as_verified() return user @@ -94,7 +70,7 @@ class SendCodeSerializer(serializers.Serializer): return value -class LoginSerializer(serializers.Serializer): +class CodeLoginSerializer(serializers.Serializer): """邮箱验证码登入 serializer""" email = serializers.EmailField() code = serializers.CharField(max_length=6, min_length=6) @@ -134,3 +110,90 @@ class LoginSerializer(serializers.Serializer): attrs['user'] = user attrs['vc'] = vc return attrs + + +class PasswordLoginSerializer(serializers.Serializer): + """邮箱/用户名 + 密码登入 serializer""" + username = serializers.CharField(required=False, allow_blank=True) + email = serializers.EmailField(required=False, allow_blank=True) + password = serializers.CharField() + + def validate(self, attrs): + """验证用户名/邮箱和密码""" + username = attrs.get('username') + email = attrs.get('email') + password = attrs.get('password') + + if not username and not email: + raise serializers.ValidationError('请输入用户名或邮箱') + + # 查找用户 + user = None + if username: + user = User.objects.filter(username=username).first() + elif email: + user = User.objects.filter(email=email).first() + + if not user: + raise serializers.ValidationError('用户不存在') + + # 验证密码 + if not user.check_password(password): + raise serializers.ValidationError('密码错误') + + attrs['user'] = user + return attrs + + +class ResetPasswordSerializer(serializers.Serializer): + """请求密码重置 serializer""" + email = serializers.EmailField() + + def validate_email(self, value): + """验证邮箱是否存在""" + if not User.objects.filter(email=value).exists(): + raise serializers.ValidationError('该邮箱未在系统中注册') + return value + + +class ConfirmResetPasswordSerializer(serializers.Serializer): + """确认密码重置 serializer""" + email = serializers.EmailField() + code = serializers.CharField(max_length=6, min_length=6) + new_password = serializers.CharField(write_only=True, min_length=6) + + def validate(self, attrs): + """验证邮箱、验证码和新密码""" + email = attrs.get('email') + code = attrs.get('code') + + # 检查用户是否存在 + try: + user = User.objects.get(email=email) + except User.DoesNotExist: + raise serializers.ValidationError('用户不存在') + + # 检查验证码 + try: + vc = VerificationCode.objects.filter(email=email).latest('created_at') + except VerificationCode.DoesNotExist: + raise serializers.ValidationError({'code': '请先获取验证码'}) + + # 检查是否被锁定 + if vc.locked_until: + from django.utils import timezone + if timezone.now() < vc.locked_until: + raise serializers.ValidationError('验证码错误次数过多,请10分钟后重试') + + # 检查验证码是否有效 + if not vc.is_valid(): + raise serializers.ValidationError({'code': '验证码已过期或已使用'}) + + # 验证码是否正确 + if vc.code != code: + vc.increment_attempts() + raise serializers.ValidationError({'code': '验证码错误'}) + + attrs['user'] = user + attrs['vc'] = vc + return attrs diff --git a/offer_backend/apps/accounts/urls.py b/offer_backend/apps/accounts/urls.py index d5202c3..043768e 100644 --- a/offer_backend/apps/accounts/urls.py +++ b/offer_backend/apps/accounts/urls.py @@ -1,12 +1,18 @@ from django.urls import path from rest_framework_simplejwt.views import TokenRefreshView -from .views import RegisterView, MeView, UserManageViewSet, UserDetailView, SendCodeView, CustomTokenObtainPairView +from .views import ( + RegisterView, MeView, UserManageViewSet, UserDetailView, + SendCodeView, CustomTokenObtainPairView, + RequestResetPasswordView, ConfirmResetPasswordView +) urlpatterns = [ path('register/', RegisterView.as_view()), path('send-code/', SendCodeView.as_view()), path('login/', CustomTokenObtainPairView.as_view()), path('token/refresh/', TokenRefreshView.as_view()), + path('reset-password/', RequestResetPasswordView.as_view()), + path('confirm-reset-password/', ConfirmResetPasswordView.as_view()), path('me/', MeView.as_view()), path('users/', UserManageViewSet.as_view()), path('users//', UserDetailView.as_view()), diff --git a/offer_backend/apps/accounts/views.py b/offer_backend/apps/accounts/views.py index 1d552f5..007c782 100644 --- a/offer_backend/apps/accounts/views.py +++ b/offer_backend/apps/accounts/views.py @@ -8,14 +8,14 @@ from django.contrib.auth import get_user_model from django.core.mail import send_mail from django.conf import settings from .models import VerificationCode -from .serializers import RegisterSerializer, UserSerializer, AdminUserSerializer, SendCodeSerializer, LoginSerializer +from .serializers import RegisterSerializer, UserSerializer, AdminUserSerializer, SendCodeSerializer, CodeLoginSerializer, PasswordLoginSerializer, ResetPasswordSerializer, ConfirmResetPasswordSerializer from .permissions import IsSuperAdmin User = get_user_model() class SendCodeView(APIView): - """发送邮箱验证码""" + """发送邮箱验证码(用于登入或密码重置)""" permission_classes = [AllowAny] def post(self, request): @@ -33,7 +33,7 @@ class SendCodeView(APIView): # 发送邮件 try: send_mail( - subject='【集团招聘平台】登入验证码', + subject='【集团招聘平台】验证码', message=f'您的验证码是:{code}\n\n验证码有效期为10分钟,请勿泄露给他人。', from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@offer.com'), recipient_list=[email], @@ -49,16 +49,75 @@ class SendCodeView(APIView): return Response({'message': '验证码已发送到您的邮箱'}) +class RequestResetPasswordView(APIView): + """请求密码重置(发送验证码)""" + permission_classes = [AllowAny] + + def post(self, request): + serializer = ResetPasswordSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + email = serializer.validated_data['email'] + + # 使用 SendCodeView 的逻辑发送验证码 + VerificationCode.objects.filter(email=email).delete() + code = VerificationCode.generate_code() + vc = VerificationCode.objects.create(email=email, code=code) + + try: + send_mail( + subject='【集团招聘平台】密码重置验证码', + message=f'您的验证码是:{code}\n\n验证码有效期为10分钟,请勿泄露给他人。', + from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@offer.com'), + recipient_list=[email], + fail_silently=False, + ) + except Exception as e: + vc.delete() + return Response( + {'error': '邮件发送失败,请稍后重试'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + return Response({'message': '验证码已发送到您的邮箱'}) + + +class ConfirmResetPasswordView(APIView): + """确认密码重置""" + permission_classes = [AllowAny] + + def post(self, request): + serializer = ConfirmResetPasswordSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + user = serializer.validated_data['user'] + vc = serializer.validated_data['vc'] + new_password = serializer.validated_data['new_password'] + + # 设置新密码 + user.set_password(new_password) + user.save() + + # 标记验证码为已使用 + vc.mark_as_verified() + + return Response({'message': '密码重置成功,请使用新密码登入'}) + + class CustomTokenObtainPairView(TokenObtainPairView): - """自定义登入视图,支持邮箱验证码和用户名密码两种方式""" + """自定义登入视图,支持三种方式:邮箱验证码、邮箱密码、用户名密码""" def post(self, request, *args, **kwargs): - # 判断是否是邮箱验证码登入(有 email 和 code)还是用户名密码登入(有 username 和 password) - if 'email' in request.data and 'code' in request.data: + # 判断登入方式 + has_code = 'code' in request.data + has_email = 'email' in request.data + has_username = 'username' in request.data + has_password = 'password' in request.data + + if has_email and has_code: # 邮箱验证码登入 return self._login_with_code(request) - elif 'username' in request.data and 'password' in request.data: - # 用户名密码登入 + elif (has_email or has_username) and has_password: + # 邮箱或用户名 + 密码登入 return self._login_with_password(request) else: return Response( @@ -68,7 +127,7 @@ class CustomTokenObtainPairView(TokenObtainPairView): def _login_with_code(self, request): """邮箱验证码登入""" - serializer = LoginSerializer(data=request.data) + serializer = CodeLoginSerializer(data=request.data) serializer.is_valid(raise_exception=True) user = serializer.validated_data['user'] @@ -86,15 +145,23 @@ class CustomTokenObtainPairView(TokenObtainPairView): }, status=status.HTTP_200_OK) def _login_with_password(self, request): - """用户名密码登入""" - from rest_framework_simplejwt.serializers import TokenObtainPairSerializer - serializer = TokenObtainPairSerializer(data=request.data) + """邮箱或用户名 + 密码登入""" + serializer = PasswordLoginSerializer(data=request.data) serializer.is_valid(raise_exception=True) - return Response(serializer.validated_data, status=status.HTTP_200_OK) + + user = serializer.validated_data['user'] + + # 生成 JWT token + from rest_framework_simplejwt.tokens import RefreshToken + refresh = RefreshToken.for_user(user) + return Response({ + 'refresh': str(refresh), + 'access': str(refresh.access_token), + }, status=status.HTTP_200_OK) class RegisterView(APIView): - """邮箱验证码注册""" + """密码注册""" permission_classes = [AllowAny] def post(self, request):