Coverage for src/accounts/serializers/user_credential.py: 65%
106 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-08-04 12:59 +0300
« prev ^ index » next coverage.py v7.9.2, created at 2025-08-04 12:59 +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.contrib.auth.tokens import default_token_generator
8from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
9from django.utils.encoding import force_bytes, force_str
10from django.conf import settings
11from django.template.loader import render_to_string
12from django.utils.html import strip_tags
14from rest_framework import serializers
16from src.common.tasks import _send_email
18from src.accounts.constants import UserErrorMessages
19from src.accounts.validators.models import (
20 UsernameValidator,
21 EmailOrUsernameValidator,
22)
24UserModel = get_user_model()
27class UserRegisterSerializer(serializers.ModelSerializer):
28 """
29 Serializer for registering a new user.
31 Inherits from ModelSerializer, which automatically generates fields based on the User model.
32 Handles validation and creation of new user instances, including password hashing and email consent.
33 """
35 # Password field: write_only means it won't be returned in API responses.
36 # trim_whitespace=False ensures passwords are not altered by removing spaces.
37 password = serializers.CharField(write_only=True, trim_whitespace=False)
39 # Consent field: write_only as it's only needed during registration.
40 agreed_to_emails = serializers.BooleanField(write_only=True)
42 class Meta:
43 model = UserModel
44 # Fields to include in the API (must match model fields or serializer fields)
45 fields = [
46 'email',
47 'username',
48 'password',
49 'agreed_to_emails',
50 ]
52 def validate_password(self, value):
53 validate_password(value)
55 return value
57 def validate_username(self, value):
58 """
59 Validate the username using a custom validator.
60 Ensures the username meets project-specific requirements (e.g., allowed characters).
61 """
62 username_validator = UsernameValidator()
64 username_validator(value)
66 return value
68 def validate_agreed_to_emails(self, value):
69 """
70 Ensure the user has agreed to receive email updates.
71 """
72 if not value:
73 raise serializers.ValidationError(
74 UserErrorMessages.AGREED_TO_EMAILS
75 )
77 return value
79 def create(self, validated_data):
80 """
81 Create a new user instance using the validated data.
82 Uses the custom manager's create_user method, which handles password hashing and other logic.
83 """
84 user = UserModel.objects.create_user(**validated_data)
86 return user
89class UserLoginRequestSerializer(serializers.Serializer):
90 """
91 Serializer for user login requests.
93 Uses a plain Serializer (not ModelSerializer) because login does not create or update model instances.
94 Accepts either email or username and a password.
95 """
97 email_or_username = serializers.CharField()
98 password = serializers.CharField()
100 def validate_email_or_username(self, value):
101 """
102 Validate the email or username using a custom validator.
103 Ensures the input is a valid email or username format.
104 """
105 email_or_username_validator = EmailOrUsernameValidator()
107 email_or_username_validator(value)
109 return value
112class UserLoginResponseSerializer(serializers.Serializer):
113 """
114 Serializer for user login responses.
116 Returns authentication tokens and a message after successful login.
117 """
119 refresh = serializers.CharField()
120 access = serializers.CharField()
121 message = serializers.CharField()
124class UserLogoutRequestSerializer(serializers.Serializer):
125 """
126 Serializer for user logout requests.
128 Accepts a refresh token to invalidate the session.
129 """
131 refresh = serializers.CharField()
134class UserLogoutResponseSerializer(serializers.Serializer):
135 """
136 Serializer for user logout responses.
138 Returns a message confirming logout.
139 """
141 message = serializers.CharField()
144class PasswordChangeSerializer(serializers.Serializer):
145 """
146 Serializer for changing a user's password.
148 Handles validation of current and new passwords.
149 """
151 current_password = serializers.CharField(
152 write_only=True, trim_whitespace=False
153 )
154 new_password = serializers.CharField(
155 write_only=True, trim_whitespace=False
156 )
158 def validate_current_password(self, value):
159 """
160 Ensure the current password matches the user's actual password.
161 """
162 user = self.context['request'].user
164 if not user.check_password(value):
165 raise serializers.ValidationError(
166 UserErrorMessages.INCORRECT_PASSWORD
167 )
169 return value
171 def validate_new_password(self, value):
172 """
173 Validate the new password using Django's and custom password validators.
174 """
175 validate_password(value, user=self.context['request'].user)
177 return value
179 def validate(self, attrs):
180 """
181 Object-level validation to ensure the new password is different from the current password.
182 """
183 if attrs['current_password'] == attrs['new_password']:
184 raise serializers.ValidationError(
185 {
186 'new_password': UserErrorMessages.NEW_PASSWORD_SAME_AS_CURRENT
187 }
188 )
190 return attrs
192 def save(self):
193 """
194 Set the new password for the user and save the user instance.
195 This method is called after validation passes.
196 """
197 user = self.context['request'].user
199 user.set_password(self.validated_data['new_password'])
201 user.save()
203 return user
206class PasswordResetRequestSerializer(serializers.Serializer):
207 email = serializers.EmailField()
209 def save(self):
210 email = self.validated_data['email']
212 try:
213 user = UserModel.objects.get(email=email, is_active=True)
215 # converts 123 to something like MTIz
216 # It's reversible -> we can decode it to get the user ID back
217 uid = urlsafe_base64_encode(force_bytes(user.pk))
219 # Creates secure tokens tied to the user
220 token = default_token_generator.make_token(user)
222 # Reset URL to provide into the email
223 reset_url = f'{settings.FRONTEND_URL}/reset-password/{uid}/{token}'
225 # Send email
226 html_message = render_to_string(
227 'mailer/reset-password.html', {'reset_url': reset_url}
228 )
229 plain_message = strip_tags(html_message)
231 _send_email.delay(
232 subject='Reset Password',
233 message=plain_message,
234 html_message=html_message,
235 from_email=settings.EMAIL_HOST_USER,
236 recipient_list=(user.email,),
237 )
239 except UserModel.DoesNotExist:
240 pass
243class PasswordResetConfirmSerializer(serializers.Serializer):
244 uid = serializers.CharField()
245 token = serializers.CharField()
246 new_password = serializers.CharField()
248 def validate(self, attrs):
249 try:
250 uid = attrs['uid']
251 token = attrs['token']
252 user_id = force_str(urlsafe_base64_decode(uid))
253 user = UserModel.objects.get(pk=user_id)
255 if not default_token_generator.check_token(user, token):
256 raise serializers.ValidationError(
257 UserErrorMessages.INVALID_TOKEN
258 )
260 attrs['user'] = user
262 return attrs
264 except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist):
265 raise serializers.ValidationError(UserErrorMessages.INVALID_TOKEN)
267 def save(self):
268 user = self.validated_data['user']
269 new_password = self.validated_data['new_password']
271 user.set_password(new_password)
272 user.save()
274 html_message = render_to_string(
275 'mailer/password-has-been-reset.html',
276 )
278 plain_message = strip_tags(html_message)
280 _send_email.delay(
281 subject='Your password has been reset',
282 message=plain_message,
283 html_message=html_message,
284 from_email=settings.EMAIL_HOST_USER,
285 recipient_list=(user.email,),
286 )