Coverage for src/accounts/serializers/user_credential.py: 66%
116 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-09-21 16:24 +0300
« prev ^ index » next coverage.py v7.9.2, created at 2025-09-21 16:24 +0300
1"""
2This module defines serializers for user registration, login, logout, and password change operations.
3"""
5from django.contrib.auth import get_user_model
6from django.contrib.auth.password_validation import validate_password
7from django.core.exceptions import ValidationError
8from django.contrib.auth.tokens import default_token_generator
9from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
10from django.utils.encoding import force_bytes, force_str
11from django.conf import settings
12from django.template.loader import render_to_string
13from django.utils.html import strip_tags
15from rest_framework import serializers
17from src.common.views import _send_email
19from src.accounts.constants import UserErrorMessages
20from src.accounts.validators.models import (
21 UsernameValidator,
22 EmailOrUsernameValidator,
23)
25UserModel = get_user_model()
28class UserRegisterSerializer(serializers.ModelSerializer):
29 """
30 Serializer for registering a new user.
32 Inherits from ModelSerializer, which automatically generates fields based on the User model.
33 Handles validation and creation of new user instances, including password hashing and email consent.
34 """
36 # Password field: write_only means it won't be returned in API responses.
37 # trim_whitespace=False ensures passwords are not altered by removing spaces.
38 password = serializers.CharField(write_only=True, trim_whitespace=False)
40 # Consent field: write_only as it's only needed during registration.
41 agreed_to_emails = serializers.BooleanField(write_only=True)
43 class Meta:
44 model = UserModel
45 # Fields to include in the API (must match model fields or serializer fields)
46 fields = [
47 'email',
48 'username',
49 'password',
50 'agreed_to_emails',
51 ]
53 def validate_password(self, value):
54 validate_password(value)
56 return value
58 def validate_username(self, value):
59 """
60 Validate the username using a custom validator.
61 Ensures the username meets project-specific requirements (e.g., allowed characters).
62 """
63 username_validator = UsernameValidator()
65 username_validator(value)
67 return value
69 def validate_agreed_to_emails(self, value):
70 """
71 Ensure the user has agreed to receive email updates.
72 """
73 if not value:
74 raise serializers.ValidationError(
75 UserErrorMessages.AGREED_TO_EMAILS
76 )
78 return value
80 def create(self, validated_data):
81 """
82 Create a new user instance using the validated data.
83 Uses the custom manager's create_user method, which handles password hashing and other logic.
84 """
85 user = UserModel.objects.create_user(**validated_data)
87 return user
90class UserLoginRequestSerializer(serializers.Serializer):
91 """
92 Serializer for user login requests.
94 Uses a plain Serializer (not ModelSerializer) because login does not create or update model instances.
95 Accepts either email or username and a password.
96 """
98 email_or_username = serializers.CharField()
99 password = serializers.CharField()
101 def validate_email_or_username(self, value):
102 """
103 Validate the email or username using a custom validator.
104 Ensures the input is a valid email or username format.
105 """
106 email_or_username_validator = EmailOrUsernameValidator()
108 email_or_username_validator(value)
110 return value
113class UserLoginResponseSerializer(serializers.Serializer):
114 """
115 Serializer for user login responses.
117 Returns authentication tokens and a message after successful login.
118 """
120 refresh = serializers.CharField()
121 access = serializers.CharField()
122 message = serializers.CharField()
125class UserLogoutRequestSerializer(serializers.Serializer):
126 """
127 Serializer for user logout requests.
129 Accepts a refresh token to invalidate the session.
130 """
132 refresh = serializers.CharField()
135class UserLogoutResponseSerializer(serializers.Serializer):
136 """
137 Serializer for user logout responses.
139 Returns a message confirming logout.
140 """
142 message = serializers.CharField()
145class UserPasswordChangeSerializer(serializers.Serializer):
146 """
147 Serializer for changing a user's password.
149 Handles validation of current and new passwords.
150 """
152 current_password = serializers.CharField(
153 write_only=True, trim_whitespace=False
154 )
155 new_password = serializers.CharField(
156 write_only=True, trim_whitespace=False
157 )
159 def validate_current_password(self, value):
160 """
161 Ensure the current password matches the user's actual password.
162 """
163 user = self.context['request'].user
165 if not user.check_password(value):
166 raise serializers.ValidationError(
167 UserErrorMessages.INCORRECT_PASSWORD
168 )
170 return value
172 def validate_new_password(self, value):
173 """
174 Validate the new password using Django's and custom password validators.
175 """
176 validate_password(value, user=self.context['request'].user)
178 return value
180 def validate(self, attrs):
181 """
182 Object-level validation to ensure the new password is different from the current password.
183 """
184 if attrs['current_password'] == attrs['new_password']:
185 raise serializers.ValidationError(
186 {
187 'new_password': UserErrorMessages.NEW_PASSWORD_SAME_AS_CURRENT
188 }
189 )
191 return attrs
193 def save(self):
194 """
195 Set the new password for the user and save the user instance.
196 This method is called after validation passes.
197 """
198 user = self.context['request'].user
200 user.set_password(self.validated_data['new_password'])
202 user.save()
204 return user
207class UserPasswordResetRequestSerializer(serializers.Serializer):
208 email = serializers.EmailField()
210 def save(self):
211 email = self.validated_data['email']
213 try:
214 user = UserModel.objects.get(email=email, is_active=True)
216 # converts 123 to something like MTIz
217 # It's reversible -> we can decode it to get the user ID back
218 uid = urlsafe_base64_encode(force_bytes(user.pk))
220 # Creates secure tokens tied to the user
221 token = default_token_generator.make_token(user)
223 # Reset URL to provide into the email
224 reset_url = f'{settings.FRONTEND_URL}/reset-password/{uid}/{token}'
226 # Send email
227 html_message = render_to_string(
228 'mailer/reset-password.html', {'reset_url': reset_url}
229 )
230 plain_message = strip_tags(html_message)
232 _send_email.delay(
233 subject='Reset Password',
234 message=plain_message,
235 html_message=html_message,
236 from_email=settings.EMAIL_HOST_USER,
237 recipient_list=(user.email,),
238 )
240 except UserModel.DoesNotExist:
241 pass
244class UserPasswordResetConfirmSerializer(serializers.Serializer):
245 # Fields expected from the frontend
246 uid = serializers.CharField()
247 token = serializers.CharField()
248 new_password = serializers.CharField()
249 confirm_new_password = serializers.CharField()
251 def validate(self, attrs):
252 try:
253 uid = attrs['uid']
254 token = attrs['token']
256 # Decode the UID to get the original user ID (e.g., from MTIz to 123)
257 user_id = force_str(urlsafe_base64_decode(uid))
259 # Try to fetch the user with the decoded ID
260 user = UserModel.objects.get(pk=user_id)
262 # Check if the token is valid for this user
263 if not default_token_generator.check_token(user, token):
264 raise serializers.ValidationError(
265 UserErrorMessages.INVALID_TOKEN
266 )
268 new_password = attrs['new_password']
269 confirm_new_password = attrs['confirm_new_password']
271 # Validate passwords match
272 if new_password != confirm_new_password:
273 raise serializers.ValidationError(
274 {
275 'confirm_new_password': UserErrorMessages.PASSWORDS_DO_NOT_MATCH,
276 }
277 )
279 # Validate new password using Django's validators
280 try:
281 validate_password(new_password, user=user)
282 except ValidationError as e:
283 raise serializers.ValidationError({'new_password': e.messages})
285 # Store the user in validated_data so we can use it in save()
286 attrs['user'] = user
288 return attrs
290 # Handle cases where UID is invalid or user doesn't exist
291 except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist):
292 raise serializers.ValidationError(UserErrorMessages.INVALID_TOKEN)
294 def save(self):
295 # Get user and new password from validated data
296 user = self.validated_data['user']
297 new_password = self.validated_data['new_password']
299 user.set_password(new_password)
300 user.save()
302 html_message = render_to_string(
303 'mailer/password-has-been-reset.html',
304 )
306 plain_message = strip_tags(html_message)
308 _send_email.delay(
309 subject='Your password has been reset',
310 message=plain_message,
311 html_message=html_message,
312 from_email=settings.EMAIL_HOST_USER,
313 recipient_list=(user.email,),
314 )