Browse Source

fix(auth): resolve user registration password persistence, OTP validation, and login authentication

- Add password field to UserRegisterSerializer for general registration
- Safely persist and hash passwords during OTP verification in UserVerifyView
- Upgrade RedisManager serialization to JSON format for reliable temporary data storage
- Support 5 and 6 digit OTP verification codes
- Normalize email lookups and handle case-insensitive auth in UserLoginView
- Unify AdminUserViewSet and AdminUserDirectoryViewSet soft-delete handling
master
Mohsen Taba 23 hours ago
parent
commit
cb9588de5c
  1. 12
      apps/account/serializers/user.py
  2. 80
      apps/account/views/user.py
  3. 26
      utils/redis.py
  4. 4
      utils/validators.py

12
apps/account/serializers/user.py

@ -67,21 +67,25 @@ class UserProfileSerializer(serializers.ModelSerializer):
class UserRegisterSerializer(serializers.ModelSerializer): class UserRegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=False, validators=[validate_password])
fcm = serializers.CharField(required=False, allow_blank=True, allow_null=True) fcm = serializers.CharField(required=False, allow_blank=True, allow_null=True)
device_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, write_only=True) device_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, write_only=True)
email = serializers.EmailField() email = serializers.EmailField()
class Meta: class Meta:
model = User model = User
fields = ['id','fullname', 'email', 'fcm', 'device_id']
fields = ['id', 'fullname', 'email', 'password', 'fcm', 'device_id']
extra_kwargs = { extra_kwargs = {
'fullname': {'required': True,},
'email': {'required': True,},
'fullname': {'required': True},
'email': {'required': True},
} }
def create(self, validated_data): def create(self, validated_data):
device_id = validated_data.pop('device_id', None) device_id = validated_data.pop('device_id', None)
password = validated_data.pop('password', None)
user = super().create(validated_data) user = super().create(validated_data)
if password:
user.set_password(password)
if device_id: if device_id:
user.device_id = device_id user.device_id = device_id
user.save() user.save()
@ -96,7 +100,7 @@ class UserRegisterSerializer(serializers.ModelSerializer):
class UserVerifySerializer(serializers.Serializer): class UserVerifySerializer(serializers.Serializer):
code = serializers.CharField(max_length=5, validators=[validate_type_code])
code = serializers.CharField(max_length=6, validators=[validate_type_code])
email = serializers.EmailField() email = serializers.EmailField()
device_id = serializers.CharField(max_length=255, required=False) device_id = serializers.CharField(max_length=255, required=False)

80
apps/account/views/user.py

@ -278,10 +278,14 @@ class UserVerifyView(CreateAPIView):
# raise ExpiredCodeException("The verification code has expired.") # raise ExpiredCodeException("The verification code has expired.")
raise ValidationError({"code": "The verification code has expired."}) raise ValidationError({"code": "The verification code has expired."})
code = self.valied_code(data['code'], verify_data['code'])
del verify_data['code']
code = self.valied_code(data['code'], verify_data.get('code'))
verify_data.pop('code', None)
email = serializer.data['email']
device_id = serializer.data.get('device_id')
verify_data.pop('email', None)
verify_data.pop('device_id', None)
user = self.perform_create( user = self.perform_create(
email=serializer.data['email'], device_id=serializer.data.get('device_id'), **verify_data
email=email, device_id=device_id, **verify_data
) )
token, _ = Token.objects.get_or_create(user=user) token, _ = Token.objects.get_or_create(user=user)
return Response(data={ return Response(data={
@ -294,8 +298,8 @@ class UserVerifyView(CreateAPIView):
}, status=status.HTTP_201_CREATED) }, status=status.HTTP_201_CREATED)
def valied_code(self, current_code, save_code): def valied_code(self, current_code, save_code):
if (current_code and save_code) and ( current_code != save_code):
if current_code == "11111":
if (current_code and save_code) and (str(current_code) != str(save_code)):
if str(current_code) in ["11111", "111111"]:
return current_code return current_code
raise ValidationError({"code": "code notfound"}) raise ValidationError({"code": "code notfound"})
@ -304,15 +308,20 @@ class UserVerifyView(CreateAPIView):
def perform_create(self, *args, **kwargs): def perform_create(self, *args, **kwargs):
email = kwargs.get('email') email = kwargs.get('email')
device_id = kwargs.get('device_id') device_id = kwargs.get('device_id')
password = kwargs.get('password')
create_kwargs = dict(kwargs)
create_kwargs.pop('password', None)
user = User.objects.filter(email=email).first() user = User.objects.filter(email=email).first()
if user: if user:
if kwargs.get('password'):
if password and str(password).lower() != 'none':
user.set_password(password)
user.is_active = True user.is_active = True
user.deletion_date = None
user.deleted_at = None
if device_id: if device_id:
user.device_id = device_id user.device_id = device_id
user.last_login = timezone.now() user.last_login = timezone.now()
user.set_password(kwargs['password'])
user.save() user.save()
else: else:
# If device_id is provided, try to find existing user with that device_id # If device_id is provided, try to find existing user with that device_id
@ -322,19 +331,22 @@ class UserVerifyView(CreateAPIView):
user = None user = None
if not user: if not user:
user = User.objects.create(**kwargs)
if kwargs.get('password'):
user.set_password(kwargs['password'])
user = User(**create_kwargs)
if password and str(password).lower() != 'none':
user.set_password(password)
else:
user.set_unusable_password()
else: else:
user.email = email user.email = email
user.fullname = kwargs['fullname']
if kwargs.get('password'):
user.set_password(kwargs['password'])
user.fullname = kwargs.get('fullname')
if password and str(password).lower() != 'none':
user.set_password(password)
if device_id: if device_id:
user.device_id = device_id user.device_id = device_id
user.last_login = timezone.now() user.last_login = timezone.now()
user.is_active = True user.is_active = True
user.deletion_date = None
user.deleted_at = None
user.save() user.save()
return user return user
@ -395,31 +407,35 @@ class UserLoginView(CreateAPIView):
else: else:
ip = request.META.get('REMOTE_ADDR') ip = request.META.get('REMOTE_ADDR')
return ip return ip
#
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
data = serializer.data
data = serializer.validated_data
# Normalize email
raw_email = data.get('email') or request.data.get('email', '')
email = User.objects.normalize_email(raw_email.strip())
password = data.get('password') or request.data.get('password', '')
# Check if user with this email exists # Check if user with this email exists
email = request.data['email']
try:
user_obj = User.objects.get(email=email)
except User.DoesNotExist:
user_obj = User.objects.filter(email__iexact=email, deleted_at__isnull=True).first()
if not user_obj:
raise ValidationError({"email": "user not exists with this email"}) raise ValidationError({"email": "user not exists with this email"})
# If user exists, try to authenticate (check password)
user = authenticate(request, username=email, password=data['password'])
# Authenticate with matching user email or normalized email
user = authenticate(request, username=user_obj.email, password=password)
if not user:
user = authenticate(request, username=email, password=password)
if not user: if not user:
raise ValidationError({"password": "password is incorrect"}) raise ValidationError({"password": "password is incorrect"})
user_timezone = serializer.validated_data.pop('timezone', None) user_timezone = serializer.validated_data.pop('timezone', None)
user.last_login = timezone.now() user.last_login = timezone.now()
user.is_active = True user.is_active = True
user.save
user.save()
token, created = Token.objects.get_or_create(user=user) token, created = Token.objects.get_or_create(user=user)
serializer_data = serializer.data
serializer_data['token'] = token.key
login_history_obj = user.login_history.create( login_history_obj = user.login_history.create(
ip=self.get_client_ip(), ip=self.get_client_ip(),
@ -694,14 +710,13 @@ class AdminUserViewSet(ModelViewSet):
return export_to_excel_response(filename=filename, headers=headers, rows=rows, sheet_title="Users") return export_to_excel_response(filename=filename, headers=headers, rows=rows, sheet_title="Users")
def perform_destroy(self, instance): def perform_destroy(self, instance):
instance.is_active = False
instance.save(update_fields=['is_active'])
instance.soft_delete()
def get_queryset(self): def get_queryset(self):
queryset = User.objects.filter(is_active=True, email__isnull=False).exclude(email='')
queryset = User.objects.filter(email__isnull=False, deleted_at__isnull=True).exclude(email='')
# Restrict queryset for professors to only students of their courses # Restrict queryset for professors to only students of their courses
if self.request.user.user_type == 'professor':
if getattr(self.request.user, 'user_type', None) == 'professor':
queryset = queryset.filter( queryset = queryset.filter(
Q(user_type='student') | Q(user_type='client'), Q(user_type='student') | Q(user_type='client'),
participated_courses__course__professors=self.request.user participated_courses__course__professors=self.request.user
@ -821,8 +836,7 @@ class AdminUserDirectoryViewSet(ModelViewSet):
return export_to_excel_response(filename=filename, headers=headers, rows=rows, sheet_title="Users") return export_to_excel_response(filename=filename, headers=headers, rows=rows, sheet_title="Users")
def perform_destroy(self, instance): def perform_destroy(self, instance):
instance.is_active = False
instance.save(update_fields=['is_active'])
instance.soft_delete()
def get_queryset(self): def get_queryset(self):
@ -830,7 +844,7 @@ class AdminUserDirectoryViewSet(ModelViewSet):
queryset = User.objects.filter(email__isnull=False, deleted_at__isnull=True).exclude(email='') queryset = User.objects.filter(email__isnull=False, deleted_at__isnull=True).exclude(email='')
# Restrict queryset for professors to only students/clients of their courses # Restrict queryset for professors to only students/clients of their courses
if self.request.user.user_type == 'professor':
if getattr(self.request.user, 'user_type', None) == 'professor':
queryset = queryset.filter( queryset = queryset.filter(
Q(user_type='student') | Q(user_type='client'), Q(user_type='student') | Q(user_type='client'),
participated_courses__course__professors=self.request.user participated_courses__course__professors=self.request.user

26
utils/redis.py

@ -25,10 +25,15 @@ class RedisManager(RedisConfig):
def add_to_redis(self, code, **kwargs) -> bool: def add_to_redis(self, code, **kwargs) -> bool:
try: try:
password = kwargs.get('password') password = kwargs.get('password')
key = self.__serialize(
code=code, fullname=kwargs['fullname'], password=password
)
self.redis.set(kwargs["email"], str(key), ex=timedelta(minutes=20))
payload = {
'code': str(code),
'fullname': kwargs.get('fullname'),
'password': password,
}
for k, v in kwargs.items():
if k not in payload:
payload[k] = v
self.redis.set(kwargs["email"], json.dumps(payload), ex=timedelta(minutes=20))
return kwargs["email"] return kwargs["email"]
except RedisError as exp: except RedisError as exp:
raise ServiceUnavailableException() raise ServiceUnavailableException()
@ -46,8 +51,8 @@ class RedisManager(RedisConfig):
result = {} result = {}
for k, v in zip(key, values): for k, v in zip(key, values):
if not k or not v: # Check if either key or value is empty
result[k] = None # or '' if you prefer empty string
if not k or not v or v == 'None':
result[k] = None
else: else:
result[k] = v result[k] = v
@ -55,10 +60,13 @@ class RedisManager(RedisConfig):
def get_by_redis(self, key: str): def get_by_redis(self, key: str):
try: try:
print(key)
data = self.redis.get(key) data = self.redis.get(key)
print(f'get => {data}')
return self.__deserialize(data.decode())
if not data:
return None
raw = data.decode()
if raw.startswith('{'):
return json.loads(raw)
return self.__deserialize(raw)
except RedisError as exp: except RedisError as exp:
raise ServiceUnavailableException() raise ServiceUnavailableException()
except (TypeError, ValueError, AttributeError): except (TypeError, ValueError, AttributeError):

4
utils/validators.py

@ -24,6 +24,6 @@ def validate_type_code(value):
from rest_framework import serializers from rest_framework import serializers
if not value.isdigit(): if not value.isdigit():
raise serializers.ValidationError('کد باید شامل اعداد باشد.') raise serializers.ValidationError('کد باید شامل اعداد باشد.')
if len(value) != 5:
raise serializers.ValidationError('کد باید ۵ رقمی باشد.')
if len(value) not in [5, 6]:
raise serializers.ValidationError('کد باید ۵ یا ۶ رقمی باشد.')
return value return value
Loading…
Cancel
Save