feat: 改为密码注册和多种密码登入方式,新增密码重置功能
后端改动: - RegisterSerializer: 改为邮箱+用户名+手机号+密码注册 - CustomTokenObtainPairView: 支持三种登入方式 * 邮箱验证码登入(验证码有效10分钟,失败5次锁定) * 邮箱+密码登入 * 用户名+密码登入 - 新增 PasswordLoginSerializer: 支持邮箱或用户名登入 - 新增 ResetPasswordSerializer: 请求密码重置 - 新增 ConfirmResetPasswordSerializer: 确认密码重置 - 新增 RequestResetPasswordView: 发送密码重置验证码 - 新增 ConfirmResetPasswordView: 重置密码 - 更新 URLs: 添加 /reset-password/ 和 /confirm-reset-password/ 功能特性: - 注册时需设置密码 - 登入可用邮箱或用户名 + 密码(邮箱和用户名对应同一密码) - 保留邮箱验证码快速登入 - 忘记密码可通过邮箱验证码重置 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b6d5a51c3d
commit
8a5ed86421
|
|
@ -6,11 +6,11 @@ User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class RegisterSerializer(serializers.Serializer):
|
class RegisterSerializer(serializers.Serializer):
|
||||||
"""邮箱验证码注册 serializer"""
|
"""密码注册 serializer"""
|
||||||
username = serializers.CharField(max_length=150)
|
username = serializers.CharField(max_length=150)
|
||||||
email = serializers.EmailField()
|
email = serializers.EmailField()
|
||||||
phone = serializers.CharField(max_length=20)
|
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):
|
def validate_username(self, value):
|
||||||
"""验证用户名是否已存在"""
|
"""验证用户名是否已存在"""
|
||||||
|
|
@ -24,39 +24,15 @@ class RegisterSerializer(serializers.Serializer):
|
||||||
raise serializers.ValidationError('邮箱已被注册')
|
raise serializers.ValidationError('邮箱已被注册')
|
||||||
return value
|
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):
|
def create(self, validated_data):
|
||||||
"""创建用户并标记验证码为已使用"""
|
"""创建用户"""
|
||||||
vc = validated_data.pop('vc')
|
|
||||||
user = User.objects.create_user(
|
user = User.objects.create_user(
|
||||||
username=validated_data['username'],
|
username=validated_data['username'],
|
||||||
email=validated_data['email'],
|
email=validated_data['email'],
|
||||||
phone=validated_data['phone'],
|
phone=validated_data['phone'],
|
||||||
|
password=validated_data['password'],
|
||||||
role='seeker'
|
role='seeker'
|
||||||
)
|
)
|
||||||
vc.mark_as_verified()
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -94,7 +70,7 @@ class SendCodeSerializer(serializers.Serializer):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class LoginSerializer(serializers.Serializer):
|
class CodeLoginSerializer(serializers.Serializer):
|
||||||
"""邮箱验证码登入 serializer"""
|
"""邮箱验证码登入 serializer"""
|
||||||
email = serializers.EmailField()
|
email = serializers.EmailField()
|
||||||
code = serializers.CharField(max_length=6, min_length=6)
|
code = serializers.CharField(max_length=6, min_length=6)
|
||||||
|
|
@ -134,3 +110,90 @@ class LoginSerializer(serializers.Serializer):
|
||||||
attrs['user'] = user
|
attrs['user'] = user
|
||||||
attrs['vc'] = vc
|
attrs['vc'] = vc
|
||||||
return attrs
|
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
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from rest_framework_simplejwt.views import TokenRefreshView
|
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 = [
|
urlpatterns = [
|
||||||
path('register/', RegisterView.as_view()),
|
path('register/', RegisterView.as_view()),
|
||||||
path('send-code/', SendCodeView.as_view()),
|
path('send-code/', SendCodeView.as_view()),
|
||||||
path('login/', CustomTokenObtainPairView.as_view()),
|
path('login/', CustomTokenObtainPairView.as_view()),
|
||||||
path('token/refresh/', TokenRefreshView.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('me/', MeView.as_view()),
|
||||||
path('users/', UserManageViewSet.as_view()),
|
path('users/', UserManageViewSet.as_view()),
|
||||||
path('users/<int:pk>/', UserDetailView.as_view()),
|
path('users/<int:pk>/', UserDetailView.as_view()),
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,14 @@ from django.contrib.auth import get_user_model
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from .models import VerificationCode
|
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
|
from .permissions import IsSuperAdmin
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class SendCodeView(APIView):
|
class SendCodeView(APIView):
|
||||||
"""发送邮箱验证码"""
|
"""发送邮箱验证码(用于登入或密码重置)"""
|
||||||
permission_classes = [AllowAny]
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
|
@ -33,7 +33,7 @@ class SendCodeView(APIView):
|
||||||
# 发送邮件
|
# 发送邮件
|
||||||
try:
|
try:
|
||||||
send_mail(
|
send_mail(
|
||||||
subject='【集团招聘平台】登入验证码',
|
subject='【集团招聘平台】验证码',
|
||||||
message=f'您的验证码是:{code}\n\n验证码有效期为10分钟,请勿泄露给他人。',
|
message=f'您的验证码是:{code}\n\n验证码有效期为10分钟,请勿泄露给他人。',
|
||||||
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@offer.com'),
|
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@offer.com'),
|
||||||
recipient_list=[email],
|
recipient_list=[email],
|
||||||
|
|
@ -49,16 +49,75 @@ class SendCodeView(APIView):
|
||||||
return Response({'message': '验证码已发送到您的邮箱'})
|
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):
|
class CustomTokenObtainPairView(TokenObtainPairView):
|
||||||
"""自定义登入视图,支持邮箱验证码和用户名密码两种方式"""
|
"""自定义登入视图,支持三种方式:邮箱验证码、邮箱密码、用户名密码"""
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
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)
|
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)
|
return self._login_with_password(request)
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -68,7 +127,7 @@ class CustomTokenObtainPairView(TokenObtainPairView):
|
||||||
|
|
||||||
def _login_with_code(self, request):
|
def _login_with_code(self, request):
|
||||||
"""邮箱验证码登入"""
|
"""邮箱验证码登入"""
|
||||||
serializer = LoginSerializer(data=request.data)
|
serializer = CodeLoginSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
user = serializer.validated_data['user']
|
user = serializer.validated_data['user']
|
||||||
|
|
@ -86,15 +145,23 @@ class CustomTokenObtainPairView(TokenObtainPairView):
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def _login_with_password(self, request):
|
def _login_with_password(self, request):
|
||||||
"""用户名密码登入"""
|
"""邮箱或用户名 + 密码登入"""
|
||||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
serializer = PasswordLoginSerializer(data=request.data)
|
||||||
serializer = TokenObtainPairSerializer(data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
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):
|
class RegisterView(APIView):
|
||||||
"""邮箱验证码注册"""
|
"""密码注册"""
|
||||||
permission_classes = [AllowAny]
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue