Skip to content

Commit 9eb0f88

Browse files
feat: Add authorization and authentication for companies.
- Update CustomJWTAuthentication to handle company tokens with version validation - Invalidate previously issued tokens for a given company upon successful authentication
1 parent 54b3ca4 commit 9eb0f88

File tree

12 files changed

+387
-27
lines changed

12 files changed

+387
-27
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Generated by Django 5.2b1 on 2025-03-25 14:18
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
initial = True
9+
10+
dependencies = []
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='Company',
15+
fields=[
16+
(
17+
'id',
18+
models.BigAutoField(
19+
auto_created=True,
20+
primary_key=True,
21+
serialize=False,
22+
verbose_name='ID',
23+
),
24+
),
25+
(
26+
'password',
27+
models.CharField(max_length=128, verbose_name='password'),
28+
),
29+
(
30+
'last_login',
31+
models.DateTimeField(
32+
blank=True, null=True, verbose_name='last login'
33+
),
34+
),
35+
('email', models.EmailField(max_length=120, unique=True)),
36+
('name', models.CharField(max_length=50)),
37+
('token_version', models.IntegerField(default=0)),
38+
('created_at', models.DateTimeField(auto_now_add=True)),
39+
('is_active', models.BooleanField(default=True)),
40+
],
41+
options={
42+
'abstract': False,
43+
},
44+
),
45+
]

promo_code/business/models.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import django.contrib.auth.models
2+
import django.db.models
3+
4+
5+
class CompanyManager(django.contrib.auth.models.BaseUserManager):
6+
def create_company(self, email, name, password=None, **extra_fields):
7+
if not email:
8+
raise ValueError('The Email must be set')
9+
10+
email = self.normalize_email(email)
11+
company = self.model(
12+
email=email,
13+
name=name,
14+
**extra_fields,
15+
)
16+
company.set_password(password)
17+
company.save(using=self._db)
18+
return company
19+
20+
21+
class Company(django.contrib.auth.models.AbstractBaseUser):
22+
email = django.db.models.EmailField(
23+
unique=True,
24+
max_length=120,
25+
)
26+
name = django.db.models.CharField(max_length=50)
27+
28+
token_version = django.db.models.IntegerField(default=0)
29+
created_at = django.db.models.DateTimeField(auto_now_add=True)
30+
is_active = django.db.models.BooleanField(default=True)
31+
32+
objects = CompanyManager()
33+
34+
USERNAME_FIELD = 'email'
35+
REQUIRED_FIELDS = ['name']
36+
37+
def __str__(self):
38+
return self.name

promo_code/business/serializers.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import business.models as business_models
2+
import business.validators
3+
import django.contrib.auth.password_validation
4+
import django.core.exceptions
5+
import django.core.validators
6+
import rest_framework.exceptions
7+
import rest_framework.serializers
8+
import rest_framework.status
9+
10+
11+
class CompanySignUpSerializer(rest_framework.serializers.ModelSerializer):
12+
password = rest_framework.serializers.CharField(
13+
write_only=True,
14+
required=True,
15+
validators=[django.contrib.auth.password_validation.validate_password],
16+
min_length=8,
17+
max_length=60,
18+
style={'input_type': 'password'},
19+
)
20+
name = rest_framework.serializers.CharField(
21+
required=True,
22+
min_length=5,
23+
max_length=50,
24+
)
25+
email = rest_framework.serializers.EmailField(
26+
required=True,
27+
min_length=8,
28+
max_length=120,
29+
validators=[
30+
business.validators.UniqueEmailValidator(
31+
'This email address is already registered.',
32+
'email_conflict',
33+
),
34+
],
35+
)
36+
37+
class Meta:
38+
model = business_models.Company
39+
fields = (
40+
'name',
41+
'email',
42+
'password',
43+
)
44+
45+
def create(self, validated_data):
46+
try:
47+
company = business_models.Company.objects.create_company(
48+
email=validated_data['email'],
49+
name=validated_data['name'],
50+
password=validated_data['password'],
51+
)
52+
company.token_version += 1
53+
company.save()
54+
return company
55+
except django.core.exceptions.ValidationError as e:
56+
raise rest_framework.serializers.ValidationError(e.messages)
57+
58+
59+
class CompanySignInSerializer(
60+
rest_framework.serializers.Serializer,
61+
):
62+
email = rest_framework.serializers.EmailField(required=True)
63+
password = rest_framework.serializers.CharField(
64+
required=True,
65+
write_only=True,
66+
style={'input_type': 'password'},
67+
)
68+
69+
def validate(self, attrs):
70+
email = attrs.get('email')
71+
password = attrs.get('password')
72+
73+
if not email or not password:
74+
raise rest_framework.exceptions.ValidationError(
75+
{'detail': 'Both email and password are required'},
76+
code='required',
77+
)
78+
79+
try:
80+
company = business_models.Company.objects.get(email=email)
81+
except business_models.Company.DoesNotExist:
82+
raise rest_framework.serializers.ValidationError(
83+
'Invalid credentials',
84+
)
85+
86+
if not company.is_active or not company.check_password(password):
87+
raise rest_framework.exceptions.AuthenticationFailed(
88+
{'detail': 'Invalid credentials or inactive account'},
89+
code='authentication_failed',
90+
)
91+
92+
return attrs

promo_code/business/urls.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import business.views
2+
import django.urls
3+
4+
app_name = 'api-business'
5+
6+
7+
urlpatterns = [
8+
django.urls.path(
9+
'auth/sign-up',
10+
business.views.CompanySignUpView.as_view(),
11+
name='company-sign-up',
12+
),
13+
django.urls.path(
14+
'auth/sign-in',
15+
business.views.CompanySignInView.as_view(),
16+
name='company-sign-in',
17+
),
18+
]

promo_code/business/validators.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import business.models
2+
import rest_framework.exceptions
3+
4+
5+
class UniqueEmailValidator:
6+
def __init__(self, default_detail=None, default_code=None):
7+
self.status_code = 409
8+
self.default_detail = (
9+
default_detail or 'This email address is already registered.'
10+
)
11+
self.default_code = default_code or 'email_conflict'
12+
13+
def __call__(self, value):
14+
if business.models.Company.objects.filter(email=value).exists():
15+
exc = rest_framework.exceptions.APIException(
16+
detail={
17+
'status': 'error',
18+
'message': self.default_detail,
19+
'code': self.default_code,
20+
},
21+
)
22+
exc.status_code = self.status_code
23+
raise exc

promo_code/business/views.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import business.models
2+
import business.serializers
3+
import rest_framework.exceptions
4+
import rest_framework.generics
5+
import rest_framework.response
6+
import rest_framework.serializers
7+
import rest_framework.status
8+
import rest_framework_simplejwt.exceptions
9+
import rest_framework_simplejwt.tokens
10+
import rest_framework_simplejwt.views
11+
12+
import core.views
13+
14+
15+
class CompanySignUpView(
16+
core.views.BaseCustomResponseMixin,
17+
rest_framework.generics.CreateAPIView,
18+
):
19+
def post(self, request):
20+
try:
21+
serializer = business.serializers.CompanySignUpSerializer(
22+
data=request.data,
23+
)
24+
serializer.is_valid(raise_exception=True)
25+
except (
26+
rest_framework.serializers.ValidationError,
27+
rest_framework_simplejwt.exceptions.TokenError,
28+
) as e:
29+
if isinstance(e, rest_framework.serializers.ValidationError):
30+
return self.handle_validation_error()
31+
32+
raise rest_framework_simplejwt.exceptions.InvalidToken(str(e))
33+
34+
company = serializer.save()
35+
36+
refresh = rest_framework_simplejwt.tokens.RefreshToken()
37+
refresh['user_type'] = 'company'
38+
refresh['company_id'] = company.id
39+
refresh['token_version'] = company.token_version
40+
41+
access_token = refresh.access_token
42+
access_token['user_type'] = 'company'
43+
access_token['company_id'] = company.id
44+
refresh['token_version'] = company.token_version
45+
46+
response_data = {
47+
'access': str(access_token),
48+
'refresh': str(refresh),
49+
}
50+
51+
return rest_framework.response.Response(
52+
response_data,
53+
status=rest_framework.status.HTTP_200_OK,
54+
)
55+
56+
57+
class CompanySignInView(
58+
core.views.BaseCustomResponseMixin,
59+
rest_framework_simplejwt.views.TokenObtainPairView,
60+
):
61+
def post(self, request):
62+
try:
63+
serializer = business.serializers.CompanySignInSerializer(
64+
data=request.data,
65+
)
66+
serializer.is_valid(raise_exception=True)
67+
except (
68+
rest_framework.serializers.ValidationError,
69+
rest_framework_simplejwt.exceptions.TokenError,
70+
) as e:
71+
if isinstance(e, rest_framework.serializers.ValidationError):
72+
return self.handle_validation_error()
73+
74+
raise rest_framework_simplejwt.exceptions.InvalidToken(str(e))
75+
76+
company = business.models.Company.objects.get(
77+
email=serializer.validated_data['email'],
78+
)
79+
company.token_version += 1
80+
company.save()
81+
82+
refresh = rest_framework_simplejwt.tokens.RefreshToken()
83+
refresh['user_type'] = 'company'
84+
refresh['company_id'] = company.id
85+
refresh['token_version'] = company.token_version
86+
87+
access_token = refresh.access_token
88+
access_token['user_type'] = 'company'
89+
access_token['company_id'] = company.id
90+
91+
response_data = {
92+
'access': str(access_token),
93+
'refresh': str(refresh),
94+
}
95+
96+
return rest_framework.response.Response(
97+
response_data,
98+
status=rest_framework.status.HTTP_200_OK,
99+
)

promo_code/core/views.py

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

78

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+
819
class PingView(django.views.View):
920
def get(self, request, *args, **kwargs):
1021
return django.http.HttpResponse('PROOOOOOOOOOOOOOOOOD', status=200)

promo_code/promo_code/urls.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import django.urls
33

44
urlpatterns = [
5-
django.urls.path('api/ping/', django.urls.include('core.urls')),
5+
django.urls.path('api/business/', django.urls.include('business.urls')),
66
django.urls.path('api/user/', django.urls.include('user.urls')),
7+
django.urls.path('api/ping/', django.urls.include('core.urls')),
78
django.urls.path('admin/', django.contrib.admin.site.urls),
89
]

0 commit comments

Comments
 (0)