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

1""" 

2This module defines serializers for user registration, login, logout, and password change operations. 

3""" 

4 

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 

13 

14from rest_framework import serializers 

15 

16from src.common.tasks import _send_email 

17 

18from src.accounts.constants import UserErrorMessages 

19from src.accounts.validators.models import ( 

20 UsernameValidator, 

21 EmailOrUsernameValidator, 

22) 

23 

24UserModel = get_user_model() 

25 

26 

27class UserRegisterSerializer(serializers.ModelSerializer): 

28 """ 

29 Serializer for registering a new user. 

30 

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 """ 

34 

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) 

38 

39 # Consent field: write_only as it's only needed during registration. 

40 agreed_to_emails = serializers.BooleanField(write_only=True) 

41 

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 ] 

51 

52 def validate_password(self, value): 

53 validate_password(value) 

54 

55 return value 

56 

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() 

63 

64 username_validator(value) 

65 

66 return value 

67 

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 ) 

76 

77 return value 

78 

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) 

85 

86 return user 

87 

88 

89class UserLoginRequestSerializer(serializers.Serializer): 

90 """ 

91 Serializer for user login requests. 

92 

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 """ 

96 

97 email_or_username = serializers.CharField() 

98 password = serializers.CharField() 

99 

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() 

106 

107 email_or_username_validator(value) 

108 

109 return value 

110 

111 

112class UserLoginResponseSerializer(serializers.Serializer): 

113 """ 

114 Serializer for user login responses. 

115 

116 Returns authentication tokens and a message after successful login. 

117 """ 

118 

119 refresh = serializers.CharField() 

120 access = serializers.CharField() 

121 message = serializers.CharField() 

122 

123 

124class UserLogoutRequestSerializer(serializers.Serializer): 

125 """ 

126 Serializer for user logout requests. 

127 

128 Accepts a refresh token to invalidate the session. 

129 """ 

130 

131 refresh = serializers.CharField() 

132 

133 

134class UserLogoutResponseSerializer(serializers.Serializer): 

135 """ 

136 Serializer for user logout responses. 

137 

138 Returns a message confirming logout. 

139 """ 

140 

141 message = serializers.CharField() 

142 

143 

144class PasswordChangeSerializer(serializers.Serializer): 

145 """ 

146 Serializer for changing a user's password. 

147 

148 Handles validation of current and new passwords. 

149 """ 

150 

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 ) 

157 

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 

163 

164 if not user.check_password(value): 

165 raise serializers.ValidationError( 

166 UserErrorMessages.INCORRECT_PASSWORD 

167 ) 

168 

169 return value 

170 

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) 

176 

177 return value 

178 

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 ) 

189 

190 return attrs 

191 

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 

198 

199 user.set_password(self.validated_data['new_password']) 

200 

201 user.save() 

202 

203 return user 

204 

205 

206class PasswordResetRequestSerializer(serializers.Serializer): 

207 email = serializers.EmailField() 

208 

209 def save(self): 

210 email = self.validated_data['email'] 

211 

212 try: 

213 user = UserModel.objects.get(email=email, is_active=True) 

214 

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)) 

218 

219 # Creates secure tokens tied to the user 

220 token = default_token_generator.make_token(user) 

221 

222 # Reset URL to provide into the email 

223 reset_url = f'{settings.FRONTEND_URL}/reset-password/{uid}/{token}' 

224 

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) 

230 

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 ) 

238 

239 except UserModel.DoesNotExist: 

240 pass 

241 

242 

243class PasswordResetConfirmSerializer(serializers.Serializer): 

244 uid = serializers.CharField() 

245 token = serializers.CharField() 

246 new_password = serializers.CharField() 

247 

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) 

254 

255 if not default_token_generator.check_token(user, token): 

256 raise serializers.ValidationError( 

257 UserErrorMessages.INVALID_TOKEN 

258 ) 

259 

260 attrs['user'] = user 

261 

262 return attrs 

263 

264 except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist): 

265 raise serializers.ValidationError(UserErrorMessages.INVALID_TOKEN) 

266 

267 def save(self): 

268 user = self.validated_data['user'] 

269 new_password = self.validated_data['new_password'] 

270 

271 user.set_password(new_password) 

272 user.save() 

273 

274 html_message = render_to_string( 

275 'mailer/password-has-been-reset.html', 

276 ) 

277 

278 plain_message = strip_tags(html_message) 

279 

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 )