Skip to content

Commit 2dd9ae0

Browse files
Merge pull request #36 from RandomProgramm3r/develop
refactor(user auth): Сonsolidate and enhance auth views, serializers, and validation - Refactor SignUpView & SignInView to use DRF generic views and SimpleJWT patterns - Simplify view logic: remove manual error handling, drop post overrides, and eliminate BaseCustomResponseMixin - Extract magic numbers/messages into top-level constants - Introduce OtherFieldSerializer for age and ISO 3166 validation - Update SignInSerializer: * @classmethod get_token with token_version claim * Atomic token_version increments via F() and update_fields * Bulk blacklist_other_tokens method - Simplify SignUpSerializer: * Use nested OtherFieldSerializer for `other` data * Properly handle Django ValidationError in create() - Cleanup: remove redundant imports"
2 parents 31754a4 + a8859af commit 2dd9ae0

File tree

7 files changed

+95
-151
lines changed

7 files changed

+95
-151
lines changed

promo_code/core/views.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,9 @@
22
import django.views
33
import rest_framework.permissions
44
import rest_framework.response
5-
import rest_framework.status
65
import rest_framework.views
76

87

9-
class BaseCustomResponseMixin:
10-
error_response = {'status': 'error', 'message': 'Error in request data.'}
11-
12-
def handle_validation_error(self):
13-
return rest_framework.response.Response(
14-
self.error_response,
15-
status=rest_framework.status.HTTP_400_BAD_REQUEST,
16-
)
17-
18-
198
class PingView(django.views.View):
209
def get(self, request, *args, **kwargs):
2110
return django.http.HttpResponse('PROOOOOOOOOOOOOOOOOD', status=200)

promo_code/user/constants.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
AGE_MIN = 0
2+
AGE_MAX = 100
3+
4+
COUNTRY_CODE_LENGTH = 2
5+
6+
PASSWORD_MIN_LENGTH = 8
7+
PASSWORD_MAX_LENGTH = 60
8+
9+
NAME_MIN_LENGTH = 1
10+
NAME_MAX_LENGTH = 100
11+
12+
SURNAME_MIN_LENGTH = 1
13+
SURNAME_MAX_LENGTH = 120
14+
15+
EMAIL_MIN_LENGTH = 8
16+
EMAIL_MAX_LENGTH = 120
17+
18+
AVATAR_URL_MAX_LENGTH = 350

promo_code/user/models.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import django.db.models
33
import django.utils.timezone
44

5+
import user.constants
6+
57

68
class UserManager(django.contrib.auth.models.BaseUserManager):
79
def create_user(self, email, name, surname, password=None, **extra_fields):
@@ -36,13 +38,20 @@ class User(
3638
django.contrib.auth.models.AbstractBaseUser,
3739
django.contrib.auth.models.PermissionsMixin,
3840
):
39-
email = django.db.models.EmailField(unique=True, max_length=120)
40-
name = django.db.models.CharField(max_length=100)
41-
surname = django.db.models.CharField(max_length=120)
41+
email = django.db.models.EmailField(
42+
unique=True,
43+
max_length=user.constants.EMAIL_MAX_LENGTH,
44+
)
45+
name = django.db.models.CharField(
46+
max_length=user.constants.NAME_MAX_LENGTH,
47+
)
48+
surname = django.db.models.CharField(
49+
max_length=user.constants.SURNAME_MAX_LENGTH,
50+
)
4251
avatar_url = django.db.models.URLField(
4352
blank=True,
4453
null=True,
45-
max_length=350,
54+
max_length=user.constants.AVATAR_URL_MAX_LENGTH,
4655
)
4756
other = django.db.models.JSONField(default=dict)
4857

promo_code/user/serializers.py

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,67 @@
11
import django.contrib.auth.password_validation
22
import django.core.exceptions
33
import django.core.validators
4+
import django.db.models
5+
import pycountry
46
import rest_framework.exceptions
57
import rest_framework.serializers
6-
import rest_framework.status
78
import rest_framework_simplejwt.serializers
89
import rest_framework_simplejwt.token_blacklist.models as tb_models
910
import rest_framework_simplejwt.tokens
1011

12+
import user.constants
1113
import user.models as user_models
1214
import user.validators
1315

1416

17+
class OtherFieldSerializer(rest_framework.serializers.Serializer):
18+
age = rest_framework.serializers.IntegerField(
19+
required=True,
20+
min_value=user.constants.AGE_MIN,
21+
max_value=user.constants.AGE_MAX,
22+
)
23+
country = rest_framework.serializers.CharField(
24+
required=True,
25+
max_length=user.constants.COUNTRY_CODE_LENGTH,
26+
min_length=user.constants.COUNTRY_CODE_LENGTH,
27+
)
28+
29+
def validate(self, value):
30+
country = value['country'].upper()
31+
32+
try:
33+
pycountry.countries.lookup(country)
34+
except LookupError:
35+
raise rest_framework.serializers.ValidationError(
36+
'Invalid ISO 3166-1 alpha-2 country code.',
37+
)
38+
39+
return value
40+
41+
1542
class SignUpSerializer(rest_framework.serializers.ModelSerializer):
1643
password = rest_framework.serializers.CharField(
1744
write_only=True,
1845
required=True,
1946
validators=[django.contrib.auth.password_validation.validate_password],
20-
max_length=60,
21-
min_length=8,
47+
max_length=user.constants.PASSWORD_MAX_LENGTH,
48+
min_length=user.constants.PASSWORD_MIN_LENGTH,
2249
style={'input_type': 'password'},
2350
)
2451
name = rest_framework.serializers.CharField(
2552
required=True,
26-
min_length=1,
27-
max_length=100,
53+
min_length=user.constants.NAME_MIN_LENGTH,
54+
max_length=user.constants.NAME_MAX_LENGTH,
2855
)
2956
surname = rest_framework.serializers.CharField(
3057
required=True,
31-
min_length=1,
32-
max_length=120,
58+
min_length=user.constants.SURNAME_MIN_LENGTH,
59+
max_length=user.constants.SURNAME_MAX_LENGTH,
3360
)
3461
email = rest_framework.serializers.EmailField(
3562
required=True,
36-
min_length=8,
37-
max_length=120,
63+
min_length=user.constants.EMAIL_MIN_LENGTH,
64+
max_length=user.constants.EMAIL_MAX_LENGTH,
3865
validators=[
3966
user.validators.UniqueEmailValidator(
4067
'This email address is already registered.',
@@ -44,15 +71,12 @@ class SignUpSerializer(rest_framework.serializers.ModelSerializer):
4471
)
4572
avatar_url = rest_framework.serializers.CharField(
4673
required=False,
47-
max_length=350,
74+
max_length=user.constants.AVATAR_URL_MAX_LENGTH,
4875
validators=[
4976
django.core.validators.URLValidator(schemes=['http', 'https']),
5077
],
5178
)
52-
other = rest_framework.serializers.JSONField(
53-
required=True,
54-
validators=[user.validators.OtherFieldValidator()],
55-
)
79+
other = OtherFieldSerializer(required=True)
5680

5781
class Meta:
5882
model = user_models.User
@@ -94,13 +118,14 @@ class SignInSerializer(
94118
def validate(self, attrs):
95119
user = self.authenticate_user(attrs)
96120

97-
self.update_token_version(user)
121+
user.token_version = django.db.models.F('token_version') + 1
122+
user.save(update_fields=['token_version'])
98123

99124
data = super().validate(attrs)
100125

101126
refresh = rest_framework_simplejwt.tokens.RefreshToken(data['refresh'])
102127

103-
self.invalidate_previous_tokens(user, refresh['jti'])
128+
self.blacklist_other_tokens(user, refresh['jti'])
104129

105130
return data
106131

@@ -128,19 +153,18 @@ def authenticate_user(self, attrs):
128153

129154
return user
130155

131-
def invalidate_previous_tokens(self, user, current_jti):
132-
outstanding_tokens = tb_models.OutstandingToken.objects.filter(
133-
user=user,
134-
).exclude(jti=current_jti)
135-
136-
for token in outstanding_tokens:
137-
tb_models.BlacklistedToken.objects.get_or_create(token=token)
138-
139-
def update_token_version(self, user):
140-
user.token_version += 1
141-
user.save()
156+
def blacklist_other_tokens(self, user, current_jti):
157+
qs = tb_models.OutstandingToken.objects.filter(user=user).exclude(
158+
jti=current_jti,
159+
)
160+
blacklisted = [tb_models.BlacklistedToken(token=tok) for tok in qs]
161+
tb_models.BlacklistedToken.objects.bulk_create(
162+
blacklisted,
163+
ignore_conflicts=True,
164+
)
142165

143-
def get_token(self, user):
166+
@classmethod
167+
def get_token(cls, user):
144168
token = super().get_token(user)
145169
token['token_version'] = user.token_version
146170
return token

promo_code/user/urls.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
urlpatterns = [
1010
django.urls.path(
1111
'auth/sign-up',
12-
user.views.SignUpView.as_view(),
12+
user.views.UserSignUpView.as_view(),
1313
name='sign-up',
1414
),
1515
django.urls.path(
1616
'auth/sign-in',
17-
rest_framework_simplejwt.views.TokenObtainPairView.as_view(),
17+
user.views.UserSignInView.as_view(),
1818
name='sign-in',
1919
),
2020
django.urls.path(

promo_code/user/validators.py

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import pycountry
21
import rest_framework.exceptions
3-
import rest_framework.serializers
42

53
import user.models
64

@@ -24,69 +22,3 @@ def __call__(self, value):
2422
)
2523
exc.status_code = self.status_code
2624
raise exc
27-
28-
29-
class OtherFieldValidator(rest_framework.serializers.Serializer):
30-
"""
31-
Validates JSON fields:
32-
- age (required, 0-100)
33-
- country (required, valid ISO 3166-1 alpha-2)
34-
"""
35-
36-
country_codes = {c.alpha_2 for c in pycountry.countries}
37-
38-
age = rest_framework.serializers.IntegerField(
39-
required=True,
40-
min_value=0,
41-
max_value=100,
42-
error_messages={
43-
'required': 'This field is required.',
44-
'invalid': 'Must be an integer.',
45-
'min_value': 'Must be between 0 and 100.',
46-
'max_value': 'Must be between 0 and 100.',
47-
},
48-
)
49-
50-
country = rest_framework.serializers.CharField(
51-
required=True,
52-
max_length=2,
53-
min_length=2,
54-
error_messages={
55-
'required': 'This field is required.',
56-
'blank': 'Must be a 2-letter ISO code.',
57-
'max_length': 'Must be a 2-letter ISO code.',
58-
'min_length': 'Must be a 2-letter ISO code.',
59-
},
60-
)
61-
62-
def validate_country(self, value):
63-
country = value.upper()
64-
if country not in self.country_codes:
65-
raise rest_framework.serializers.ValidationError(
66-
'Invalid ISO 3166-1 alpha-2 country code.',
67-
)
68-
69-
return country
70-
71-
def __call__(self, value):
72-
if not isinstance(value, dict):
73-
raise rest_framework.serializers.ValidationError(
74-
{'non_field_errors': ['Must be a JSON object']},
75-
)
76-
77-
missing_fields = [
78-
field
79-
for field in self.fields
80-
if field not in value or value.get(field) in (None, '')
81-
]
82-
83-
if missing_fields:
84-
raise rest_framework.serializers.ValidationError(
85-
dict.fromkeys(missing_fields, 'This field is required.'),
86-
)
87-
88-
serializer = self.__class__(data=value)
89-
if not serializer.is_valid():
90-
raise rest_framework.serializers.ValidationError(serializer.errors)
91-
92-
return value

promo_code/user/views.py

Lines changed: 10 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,39 @@
1-
import rest_framework.exceptions
21
import rest_framework.generics
32
import rest_framework.response
4-
import rest_framework.serializers
53
import rest_framework.status
6-
import rest_framework_simplejwt.exceptions
74
import rest_framework_simplejwt.tokens
85
import rest_framework_simplejwt.views
96

10-
import core.views
117
import user.serializers
128

139

14-
class SignUpView(
15-
core.views.BaseCustomResponseMixin,
10+
class UserSignUpView(
1611
rest_framework.generics.CreateAPIView,
1712
):
1813
serializer_class = user.serializers.SignUpSerializer
1914

2015
def create(self, request, *args, **kwargs):
2116
serializer = self.get_serializer(data=request.data)
22-
23-
try:
24-
serializer.is_valid(raise_exception=True)
25-
except rest_framework.exceptions.ValidationError:
26-
return self.handle_validation_error()
17+
serializer.is_valid(raise_exception=True)
2718

2819
user = serializer.save()
29-
3020
refresh = rest_framework_simplejwt.tokens.RefreshToken.for_user(user)
3121
refresh['token_version'] = user.token_version
22+
3223
access_token = refresh.access_token
3324

25+
response_data = {
26+
'access': str(access_token),
27+
'refresh': str(refresh),
28+
}
29+
3430
return rest_framework.response.Response(
35-
{'access': str(access_token), 'refresh': str(refresh)},
31+
response_data,
3632
status=rest_framework.status.HTTP_200_OK,
3733
)
3834

3935

40-
class SignInView(
41-
core.views.BaseCustomResponseMixin,
36+
class UserSignInView(
4237
rest_framework_simplejwt.views.TokenObtainPairView,
4338
):
4439
serializer_class = user.serializers.SignInSerializer
45-
46-
def post(self, request, *args, **kwargs):
47-
try:
48-
serializer = self.get_serializer(data=request.data)
49-
serializer.is_valid(raise_exception=True)
50-
except (
51-
rest_framework.serializers.ValidationError,
52-
rest_framework_simplejwt.exceptions.TokenError,
53-
) as e:
54-
if isinstance(e, rest_framework.serializers.ValidationError):
55-
return self.handle_validation_error()
56-
57-
raise rest_framework_simplejwt.exceptions.InvalidToken(str(e))
58-
59-
response_data = {
60-
'access': serializer.validated_data['access'],
61-
'refresh': serializer.validated_data['refresh'],
62-
}
63-
64-
return rest_framework.response.Response(
65-
response_data,
66-
status=rest_framework.status.HTTP_200_OK,
67-
)

0 commit comments

Comments
 (0)