152 lines
6.3 KiB
Python
152 lines
6.3 KiB
Python
from django.db import transaction
|
|
from django.utils import timezone
|
|
from rest_framework import serializers, status
|
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
|
|
from rest_framework_simplejwt.tokens import RefreshToken
|
|
|
|
from apps.cuser.models import CUser, CuserRole, RoleType
|
|
from apps.jwt_auth.models import JwtToken, CUserRoleLoginOpt
|
|
from apps.jwt_auth.services import login_by_username, login_by_mobile, verify_mobile_and_get_token
|
|
from apps.jwt_auth.session import issue_tokens, validate_session_claims
|
|
from apps.workspace.models import Workspace
|
|
|
|
|
|
class RoleTypeSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = RoleType
|
|
fields = ('role_name', 'role_name_en')
|
|
|
|
|
|
class RoleSerializer(serializers.ModelSerializer):
|
|
class WorkspaceSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = Workspace
|
|
fields = ('id', 'name')
|
|
|
|
role_type = RoleTypeSerializer()
|
|
sys_ws = WorkspaceSerializer()
|
|
|
|
class Meta:
|
|
model = CuserRole
|
|
fields = ('name_family', 'title', 'role_type', 'sys_ws')
|
|
|
|
|
|
class LoginOptionSerializer(serializers.ModelSerializer):
|
|
cuser_role = RoleSerializer()
|
|
|
|
class Meta:
|
|
model = CUserRoleLoginOpt
|
|
fields = ('cuser_role', 'expire', 'uuid')
|
|
|
|
|
|
class PublicAuthView(APIView):
|
|
authentication_classes = ()
|
|
permission_classes = (AllowAny,)
|
|
|
|
def get_authenticate_header(self, request):
|
|
return 'Bearer realm="api"'
|
|
|
|
|
|
class VerifyMobileAndGetTokenView(PublicAuthView):
|
|
class InputSerializer(serializers.Serializer):
|
|
mobile = serializers.CharField(required=True)
|
|
opt = serializers.IntegerField(required=True, min_value=100000, max_value=999999)
|
|
|
|
def post(self, request):
|
|
serializer = self.InputSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
options = verify_mobile_and_get_token(**serializer.validated_data)
|
|
return Response(LoginOptionSerializer(options, many=True).data)
|
|
|
|
|
|
class LoginByMobileView(PublicAuthView):
|
|
class InputSerializer(serializers.Serializer):
|
|
mobile = serializers.CharField(required=True)
|
|
|
|
def post(self, request):
|
|
serializer = self.InputSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
result = login_by_mobile(**serializer.validated_data)
|
|
return Response({'result': result}, status=202 if result['delivery_status'] == 'unconfirmed' else 200)
|
|
|
|
|
|
class LoginByUserView(PublicAuthView):
|
|
class InputSerializer(serializers.Serializer):
|
|
username = serializers.CharField(required=True)
|
|
password = serializers.CharField(required=True, trim_whitespace=False, write_only=True)
|
|
|
|
def post(self, request):
|
|
serializer = self.InputSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
options = login_by_username(**serializer.validated_data)
|
|
return Response(LoginOptionSerializer(options, many=True).data)
|
|
|
|
|
|
class CustomTokenObtainPairView(PublicAuthView):
|
|
class InputSerializer(serializers.Serializer):
|
|
uuid_opt = serializers.UUIDField(required=True)
|
|
|
|
@transaction.atomic
|
|
def post(self, request):
|
|
serializer = self.InputSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
try:
|
|
user_id = CUserRoleLoginOpt.objects.values_list('cuser_id', flat=True).get(
|
|
uuid=serializer.validated_data['uuid_opt'], expire__gt=timezone.now())
|
|
CUser.objects.select_for_update().get(pk=user_id)
|
|
option = CUserRoleLoginOpt.objects.select_related(
|
|
'cuser_role__cuser', 'cuser_role__role_type', 'cuser_role__sys_ws',
|
|
).get(uuid=serializer.validated_data['uuid_opt'], expire__gt=timezone.now())
|
|
except (CUserRoleLoginOpt.DoesNotExist, CUser.DoesNotExist):
|
|
raise InvalidToken('اعتبار درخواست ورود تمام شده است. دوباره وارد شوید.')
|
|
if option.cuser_id != option.cuser_role.cuser_id:
|
|
raise InvalidToken('درخواست ورود معتبر نیست.')
|
|
data = issue_tokens(option.cuser_role)
|
|
CUserRoleLoginOpt.objects.filter(cuser_id=option.cuser_id).delete()
|
|
return Response(data)
|
|
|
|
|
|
class CustomTokenRefreshView(PublicAuthView):
|
|
class InputSerializer(serializers.Serializer):
|
|
refresh = serializers.CharField(required=True)
|
|
|
|
@transaction.atomic
|
|
def post(self, request):
|
|
serializer = self.InputSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
refresh_value = serializer.validated_data['refresh']
|
|
try:
|
|
refresh = RefreshToken(refresh_value)
|
|
# Nullable user/workspace joins must not participate in PostgreSQL's
|
|
# row lock. The session alone serializes refresh-token rotation.
|
|
session = JwtToken.objects.select_for_update(of=('self',)).select_related(
|
|
'cuser_role__cuser', 'cuser_role__role_type', 'cuser_role__sys_ws',
|
|
).get(refresh=refresh_value, is_blocked=False)
|
|
except (TokenError, JwtToken.DoesNotExist):
|
|
raise InvalidToken('نشست شما منقضی یا باطل شده است. دوباره وارد شوید.')
|
|
validate_session_claims(session, refresh)
|
|
return Response(issue_tokens(session.cuser_role, session=session))
|
|
|
|
|
|
class LogoutView(PublicAuthView):
|
|
class InputSerializer(serializers.Serializer):
|
|
refresh = serializers.CharField(required=True)
|
|
|
|
def post(self, request):
|
|
serializer = self.InputSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
value = serializer.validated_data['refresh']
|
|
try:
|
|
token = RefreshToken(value)
|
|
except TokenError:
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
# Possession of the signed refresh token permits logout even after access expires.
|
|
if type(token.get('session_id')) is int and type(token.get('user_id')) is int:
|
|
JwtToken.objects.filter(pk=token['session_id'], cuser_id=token['user_id']).update(is_blocked=True)
|
|
else:
|
|
JwtToken.objects.filter(refresh=value).update(is_blocked=True)
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|