working on manual logout: block access token
This commit is contained in:
@@ -80,6 +80,7 @@ MIDDLEWARE = [
|
|||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'apps.authentication.middlewares.BlockedTokenMiddleware',
|
||||||
'crum.CurrentRequestUserMiddleware',
|
'crum.CurrentRequestUserMiddleware',
|
||||||
'apps.log.middlewares.SaveLog'
|
'apps.log.middlewares.SaveLog'
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from apps.authentication.api.v1.serializers.serializer import (
|
|||||||
OrganizationTypeSerializer,
|
OrganizationTypeSerializer,
|
||||||
OrganizationSerializer,
|
OrganizationSerializer,
|
||||||
UserSerializer,
|
UserSerializer,
|
||||||
BankAccountSerializer
|
BankAccountSerializer,
|
||||||
)
|
)
|
||||||
from rest_framework_simplejwt.views import TokenObtainPairView
|
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||||
from apps.authorization.api.v1 import api as authorize_view
|
from apps.authorization.api.v1 import api as authorize_view
|
||||||
@@ -21,7 +21,8 @@ from apps.authentication.models import (
|
|||||||
Province,
|
Province,
|
||||||
Organization,
|
Organization,
|
||||||
OrganizationType,
|
OrganizationType,
|
||||||
BankAccountInformation
|
BankAccountInformation,
|
||||||
|
BlacklistedAccessToken
|
||||||
)
|
)
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@@ -30,6 +31,9 @@ from django.core.cache import cache
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from common.sms import send_sms
|
from common.sms import send_sms
|
||||||
import random
|
import random
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from apps.authentication.tools import get_token_jti
|
||||||
|
|
||||||
|
|
||||||
class CustomizedTokenObtainPairView(TokenObtainPairView):
|
class CustomizedTokenObtainPairView(TokenObtainPairView):
|
||||||
@@ -37,6 +41,24 @@ class CustomizedTokenObtainPairView(TokenObtainPairView):
|
|||||||
serializer_class = CustomizedTokenObtainPairSerializer
|
serializer_class = CustomizedTokenObtainPairSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class LogoutView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
token_str = request.auth # access token from header
|
||||||
|
jti, user_id = get_token_jti(str(token_str))
|
||||||
|
|
||||||
|
if not jti:
|
||||||
|
return Response({'detail': 'Invalid token'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
BlacklistedAccessToken.objects.get_or_create(jti=jti, defaults={
|
||||||
|
'token': token_str,
|
||||||
|
'user_id': user_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({'detail': 'Access token blacklisted.'}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(ModelViewSet):
|
class UserViewSet(ModelViewSet):
|
||||||
""" Crud operations for user model """
|
""" Crud operations for user model """
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ from .api import (
|
|||||||
ProvinceViewSet,
|
ProvinceViewSet,
|
||||||
OrganizationViewSet,
|
OrganizationViewSet,
|
||||||
OrganizationTypeViewSet,
|
OrganizationTypeViewSet,
|
||||||
GeneralOTPViewSet
|
GeneralOTPViewSet,
|
||||||
|
LogoutView
|
||||||
)
|
)
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
@@ -26,6 +27,7 @@ router.register(r'otp', GeneralOTPViewSet, basename='otp')
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('login/', CustomizedTokenObtainPairView.as_view(), name='token_obtain_pair'),
|
path('login/', CustomizedTokenObtainPairView.as_view(), name='token_obtain_pair'),
|
||||||
|
path('logout/', LogoutView.as_view(), name='logut'),
|
||||||
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||||
path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
|
path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
|
||||||
path('token/revoke/', TokenBlacklistView.as_view(), name='revoke_token'),
|
path('token/revoke/', TokenBlacklistView.as_view(), name='revoke_token'),
|
||||||
|
|||||||
9
apps/authentication/exceptions.py
Normal file
9
apps/authentication/exceptions.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from rest_framework.exceptions import APIException
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
|
||||||
|
class TokenBlackListedException(APIException):
|
||||||
|
status_code = status.HTTP_401_UNAUTHORIZED
|
||||||
|
default_detail = _('unauthorized')
|
||||||
|
default_code = 'unauthorized'
|
||||||
25
apps/authentication/middlewares.py
Normal file
25
apps/authentication/middlewares.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
from .models import BlacklistedAccessToken
|
||||||
|
from apps.authentication.tools import get_token_jti
|
||||||
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
|
from apps.authentication.exceptions import TokenBlackListedException
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
|
||||||
|
class BlockedTokenMiddleware:
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
auth_header = request.headers.get('Authorization')
|
||||||
|
if auth_header and auth_header.startswith('Bearer '):
|
||||||
|
token_str = auth_header[7:]
|
||||||
|
jti, _ = get_token_jti(token_str)
|
||||||
|
if jti and BlacklistedAccessToken.objects.filter(jti=jti).exists():
|
||||||
|
return JsonResponse({
|
||||||
|
'detail': 'Access token has been blacklisted'
|
||||||
|
}, status=401)
|
||||||
|
|
||||||
|
return self.get_response(request)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.0 on 2025-06-02 08:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('authentication', '0019_organizationtype_code'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BlacklistedAccessToken',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('jti', models.CharField(max_length=255, unique=True)),
|
||||||
|
('token', models.TextField()),
|
||||||
|
('user_id', models.IntegerField()),
|
||||||
|
('blacklisted_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -144,3 +144,13 @@ class BankAccountInformation(BaseModel):
|
|||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
super(BankAccountInformation, self).save(*args, **kwargs)
|
super(BankAccountInformation, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class BlacklistedAccessToken(models.Model):
|
||||||
|
jti = models.CharField(max_length=255, unique=True)
|
||||||
|
token = models.TextField()
|
||||||
|
user_id = models.IntegerField()
|
||||||
|
blacklisted_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Blacklisted JTI: {self.jti}"
|
||||||
|
|||||||
9
apps/authentication/tools.py
Normal file
9
apps/authentication/tools.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from rest_framework_simplejwt.tokens import AccessToken
|
||||||
|
|
||||||
|
|
||||||
|
def get_token_jti(token_str):
|
||||||
|
try:
|
||||||
|
token = AccessToken(token_str)
|
||||||
|
return token['jti'], token['user_id']
|
||||||
|
except Exception as e:
|
||||||
|
return None, None
|
||||||
@@ -3,14 +3,16 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
|
|||||||
from apps.authorization.api.v1.serializers import (
|
from apps.authorization.api.v1.serializers import (
|
||||||
RoleSerializer,
|
RoleSerializer,
|
||||||
PermissionSerializer,
|
PermissionSerializer,
|
||||||
UserRelationSerializer
|
UserRelationSerializer,
|
||||||
|
PageSerializer
|
||||||
)
|
)
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from apps.authorization.models import (
|
from apps.authorization.models import (
|
||||||
Role,
|
Role,
|
||||||
Permissions,
|
Permissions,
|
||||||
UserRelations
|
UserRelations,
|
||||||
|
Page
|
||||||
)
|
)
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
@@ -23,27 +25,22 @@ class RoleViewSet(viewsets.ModelViewSet):
|
|||||||
serializer_class = RoleSerializer
|
serializer_class = RoleSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class PageViewSet(viewsets.ModelViewSet):
|
||||||
|
""" add website pages to system to set permission on it """
|
||||||
|
|
||||||
|
queryset = Page.objects.all()
|
||||||
|
serializer_class = PageSerializer
|
||||||
|
|
||||||
|
|
||||||
class PermissionViewSet(viewsets.ModelViewSet):
|
class PermissionViewSet(viewsets.ModelViewSet):
|
||||||
""" Crud Operations for Permissions """
|
""" Crud Operations for Permissions """
|
||||||
|
|
||||||
queryset = Permissions.objects.all()
|
queryset = Permissions.objects.all()
|
||||||
serializer_class = PermissionSerializer
|
serializer_class = PermissionSerializer
|
||||||
|
|
||||||
@action(
|
|
||||||
methods=['get'],
|
|
||||||
detail=False,
|
|
||||||
url_path='get_user_permissions',
|
|
||||||
url_name='get_user_permissions',
|
|
||||||
name='get_user_permissions'
|
|
||||||
)
|
|
||||||
@transaction.atomic
|
|
||||||
def get_user_permissions(self, request):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UserRelationViewSet(viewsets.ModelViewSet):
|
class UserRelationViewSet(viewsets.ModelViewSet):
|
||||||
""" Crud Operations for User Relations """
|
""" Crud Operations for User Relations """
|
||||||
|
|
||||||
queryset = UserRelations.objects.all()
|
queryset = UserRelations.objects.all()
|
||||||
serializer_class = UserRelationSerializer
|
serializer_class = UserRelationSerializer
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from apps.authorization.models import (
|
from apps.authorization.models import (
|
||||||
Role,
|
Role,
|
||||||
Permissions,
|
Permissions,
|
||||||
UserRelations
|
UserRelations,
|
||||||
|
Page
|
||||||
)
|
)
|
||||||
from apps.authentication.api.v1.serializers import serializer as auth_serializer
|
from apps.authentication.api.v1.serializers import serializer as auth_serializer
|
||||||
from apps.authentication.models import Organization
|
from apps.authentication.models import Organization
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
|
|
||||||
|
class PageSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Page
|
||||||
|
fields = [
|
||||||
|
'name',
|
||||||
|
'code'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class PermissionSerializer(serializers.ModelSerializer):
|
class PermissionSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Permissions
|
model = Permissions
|
||||||
@@ -26,6 +38,18 @@ class PermissionSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
return representation
|
return representation
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def permissions_structure_output(cls, permissions: list) -> typing.Any:
|
||||||
|
""" set a structure for permissions """
|
||||||
|
structure = {}
|
||||||
|
for permission in permissions:
|
||||||
|
if permission.page.name not in structure.keys():
|
||||||
|
structure.update(
|
||||||
|
{f'{permission.page.name}': itertools.chain(*list(
|
||||||
|
permission.page.permission_page.all().values_list('name')))
|
||||||
|
})
|
||||||
|
return structure
|
||||||
|
|
||||||
|
|
||||||
class RoleSerializer(serializers.ModelSerializer):
|
class RoleSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -49,15 +73,7 @@ class RoleSerializer(serializers.ModelSerializer):
|
|||||||
representation['type'] = auth_serializer.OrganizationTypeSerializer(instance.type).data
|
representation['type'] = auth_serializer.OrganizationTypeSerializer(instance.type).data
|
||||||
if instance.permissions: # noqa
|
if instance.permissions: # noqa
|
||||||
permissions = instance.permissions.all()
|
permissions = instance.permissions.all()
|
||||||
pages = {}
|
representation['permissions'] = PermissionSerializer().permissions_structure_output(permissions)
|
||||||
for permission in permissions:
|
|
||||||
if permission.page.name not in pages.keys():
|
|
||||||
pages.update({
|
|
||||||
f'{permission.page.name}': itertools.chain(*list(
|
|
||||||
(permission.page.permission_page.all().values_list('name'))
|
|
||||||
))
|
|
||||||
})
|
|
||||||
representation['permissions'] = pages
|
|
||||||
return representation
|
return representation
|
||||||
|
|
||||||
|
|
||||||
@@ -73,6 +89,7 @@ class UserRelationSerializer(serializers.ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
|
""" custom output for serializer """
|
||||||
representation = super().to_representation(instance)
|
representation = super().to_representation(instance)
|
||||||
if isinstance(instance, UserRelations):
|
if isinstance(instance, UserRelations):
|
||||||
if instance.user:
|
if instance.user:
|
||||||
@@ -81,18 +98,11 @@ class UserRelationSerializer(serializers.ModelSerializer):
|
|||||||
representation['organization'] = auth_serializer.OrganizationSerializer(instance.organization).data
|
representation['organization'] = auth_serializer.OrganizationSerializer(instance.organization).data
|
||||||
if instance.role:
|
if instance.role:
|
||||||
representation['role'] = RoleSerializer(instance.role).data
|
representation['role'] = RoleSerializer(instance.role).data
|
||||||
if instance.permissions:
|
if instance.permissions: # noqa
|
||||||
|
# set permissions by a default structure like:
|
||||||
|
# 'page permission':[element permissions]
|
||||||
permissions = instance.permissions.all()
|
permissions = instance.permissions.all()
|
||||||
pages = {}
|
representation['permissions'] = PermissionSerializer().permissions_structure_output(permissions)
|
||||||
for permission in permissions:
|
|
||||||
if permission.page.name not in pages.keys():
|
|
||||||
pages.update({
|
|
||||||
f'{permission.page.name}': itertools.chain(*list(
|
|
||||||
(permission.page.permission_page.all().values_list('name'))
|
|
||||||
))
|
|
||||||
})
|
|
||||||
representation['permissions'] = pages
|
|
||||||
|
|
||||||
return representation
|
return representation
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
@@ -100,9 +110,9 @@ class UserRelationSerializer(serializers.ModelSerializer):
|
|||||||
if validated_data.get('role'):
|
if validated_data.get('role'):
|
||||||
instance.role = Role.objects.get(id=validated_data.get('role', instance.role))
|
instance.role = Role.objects.get(id=validated_data.get('role', instance.role))
|
||||||
if validated_data.get('organization'):
|
if validated_data.get('organization'):
|
||||||
instance.organization = Organization.objects.get(id=validated_data.get(
|
instance.organization = Organization.objects.get(
|
||||||
'organization', instance.organization
|
id=validated_data.get('organization', instance.organization)
|
||||||
))
|
)
|
||||||
instance.save()
|
instance.save()
|
||||||
instance.permissions.clear()
|
instance.permissions.clear()
|
||||||
instance.permissions.add(*(validated_data.get('permissions', instance.permissions)))
|
instance.permissions.add(*(validated_data.get('permissions', instance.permissions)))
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ from django.urls import path, include
|
|||||||
from .api import (
|
from .api import (
|
||||||
RoleViewSet,
|
RoleViewSet,
|
||||||
PermissionViewSet,
|
PermissionViewSet,
|
||||||
UserRelationViewSet
|
UserRelationViewSet,
|
||||||
|
PageViewSet
|
||||||
)
|
)
|
||||||
|
|
||||||
router = DefaultRouter() # set router
|
router = DefaultRouter() # set router
|
||||||
@@ -12,6 +13,7 @@ router = DefaultRouter() # set router
|
|||||||
router.register(r'role', RoleViewSet, basename='role')
|
router.register(r'role', RoleViewSet, basename='role')
|
||||||
router.register(r'permission', PermissionViewSet, basename='permission')
|
router.register(r'permission', PermissionViewSet, basename='permission')
|
||||||
router.register(r'user-relations', UserRelationViewSet, basename='organization-role')
|
router.register(r'user-relations', UserRelationViewSet, basename='organization-role')
|
||||||
|
router.register(r'page', PageViewSet, basename='page')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', include(router.urls))
|
path('', include(router.urls))
|
||||||
|
|||||||
Reference in New Issue
Block a user