add product
This commit is contained in:
1 parent
423d4cf40e
commit
c8d45dd07c
58 files changed
+8309
-674
No files matched your search
+34
-12
@@ -1,17 +1,39 @@
|
||||
from django.contrib import admin
|
||||
from apps.jwt_auth.models import (
|
||||
JwtToken as JwtTokenModel,
|
||||
CUserRoleLoginOpt as CUserRoleLoginOptModel,
|
||||
)
|
||||
|
||||
# Register your models here.
|
||||
from apps.jwt_auth.models import JwtToken, CUserRoleLoginOpt
|
||||
from utils.admin_base import CRMModelAdmin
|
||||
|
||||
|
||||
@admin.register(JwtTokenModel)
|
||||
class JwtTokenAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
@admin.register(JwtToken)
|
||||
class JwtTokenAdmin(CRMModelAdmin):
|
||||
list_display = ('id', 'cuser', 'cuser_role', 'expire', 'is_blocked')
|
||||
list_filter = ('is_blocked', 'cuser_role__sys_ws')
|
||||
search_fields = ('cuser__username', 'cuser__mobile')
|
||||
exclude = ('refresh', 'access')
|
||||
sensitive_fields = ('refresh', 'access')
|
||||
readonly_fields = ('cuser', 'cuser_role', 'expire', 'is_blocked')
|
||||
actions = ('revoke_sessions', 'export_selected')
|
||||
|
||||
@admin.action(description='ابطال نشستهای انتخابشده', permissions=['change'])
|
||||
def revoke_sessions(self, request, queryset):
|
||||
count = queryset.update(is_blocked=True)
|
||||
for obj in queryset:
|
||||
self.log_change(request, obj, 'نشست از پنل مدیریت ابطال شد.')
|
||||
self.message_user(request, f'{count} نشست ابطال شد.')
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
|
||||
@admin.register(CUserRoleLoginOptModel)
|
||||
class CUserRoleLoginOptAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
@admin.register(CUserRoleLoginOpt)
|
||||
class CUserRoleLoginOptAdmin(CRMModelAdmin):
|
||||
list_display = ('id', 'cuser', 'cuser_role', 'expire')
|
||||
search_fields = ('cuser__username', 'cuser__mobile')
|
||||
exclude = ('uuid',)
|
||||
sensitive_fields = ('uuid',)
|
||||
readonly_fields = ('cuser', 'cuser_role', 'expire')
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
@@ -0,0 +1,19 @@
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
|
||||
from apps.jwt_auth.models import JwtToken
|
||||
from apps.jwt_auth.session import validate_session_claims
|
||||
|
||||
|
||||
class SessionJWTAuthentication(JWTAuthentication):
|
||||
"""A signed JWT is usable only while its recorded session remains active."""
|
||||
|
||||
def get_user(self, validated_token):
|
||||
user = super().get_user(validated_token)
|
||||
session = JwtToken.objects.select_related('cuser_role__role_type', 'cuser_role__sys_ws', 'cuser_role__cuser').filter(
|
||||
access=str(validated_token), cuser=user, is_blocked=False,
|
||||
).first()
|
||||
if not session:
|
||||
raise AuthenticationFailed('نشست شما معتبر نیست. لطفاً دوباره وارد شوید.', code='session_invalid')
|
||||
validate_session_claims(session, validated_token)
|
||||
return user
|
||||
@@ -0,0 +1,12 @@
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [('jwt_auth', '0092_alter_cuserroleloginopt_uuid')]
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='cuserroleloginopt', name='uuid',
|
||||
field=models.UUIDField(default=uuid.uuid4, unique=True),
|
||||
),
|
||||
]
|
||||
@@ -22,4 +22,4 @@ class CUserRoleLoginOpt(models.Model):
|
||||
cuser_role = models.ForeignKey(CuserRoleModel, on_delete=models.CASCADE)
|
||||
cuser = models.ForeignKey(CUser, null=True, blank=True, default=None, on_delete=models.CASCADE)
|
||||
expire = models.DateTimeField(null=False, blank=False)
|
||||
uuid = models.UUIDField(default=uuid.uuid4(), unique=True, null=False, blank=False)
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True, null=False, blank=False)
|
||||
+87
-98
@@ -1,110 +1,99 @@
|
||||
import uuid
|
||||
import random
|
||||
import secrets
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from apps.jwt_auth.models import CUserRoleLoginOpt
|
||||
from apps.cuser.models import CuserRole as CuserRoleModel, SmsOpt as SmsOptModel
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from rest_framework import exceptions
|
||||
import requests
|
||||
from datetime import timedelta
|
||||
|
||||
from apps.cuser.models import CuserRole, SmsOpt
|
||||
from apps.jwt_auth.models import CUserRoleLoginOpt
|
||||
from utils.services.sms import DefaultSmsProvider
|
||||
from utils.validations.mobile import normalize_mobile
|
||||
|
||||
CUser = get_user_model()
|
||||
|
||||
def login_by_username(username: str, password: str) -> CUserRoleLoginOpt:
|
||||
|
||||
class SmsDeliveryUnavailable(exceptions.APIException):
|
||||
status_code = 503
|
||||
default_detail = 'ارسال پیامک انجام نشد. لطفاً کمی بعد دوباره تلاش کنید.'
|
||||
default_code = 'sms_failed'
|
||||
|
||||
|
||||
def eligible_roles(user, customer_only=False):
|
||||
roles = CuserRole.objects.select_related('role_type', 'sys_ws', 'cuser').filter(
|
||||
cuser=user, is_active=True, mobile_verify=True, sys_ws__is_active=True,
|
||||
)
|
||||
if customer_only:
|
||||
roles = roles.filter(role_type__role_name_en='workspace_customer', customer_panel=True)
|
||||
return [role for role in roles if
|
||||
(not role.sys_ws.expire or role.sys_ws.expire > timezone.now()) and
|
||||
(role.role_type.role_name_en != 'workspace_customer' or role.customer_panel is True)]
|
||||
|
||||
|
||||
def create_login_options(user, roles):
|
||||
CUserRoleLoginOpt.objects.filter(cuser=user).delete()
|
||||
options = []
|
||||
for role in roles:
|
||||
options.append(CUserRoleLoginOpt.objects.create(
|
||||
cuser=user, cuser_role=role, expire=timezone.now() + timedelta(minutes=5),
|
||||
))
|
||||
return options
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def login_by_username(username: str, password: str):
|
||||
user = CUser.objects.select_for_update().filter(username=username).first()
|
||||
if not user or not user.check_password(password):
|
||||
raise exceptions.ValidationError({'error': 'نام کاربری یا رمز عبور اشتباه است.'})
|
||||
if not user.is_active:
|
||||
raise exceptions.PermissionDenied('حساب کاربری شما غیرفعال است.')
|
||||
roles = eligible_roles(user)
|
||||
if not roles:
|
||||
raise exceptions.PermissionDenied('نقش یا دسترسی فعال برای این حساب وجود ندارد.')
|
||||
return create_login_options(user, roles)
|
||||
|
||||
|
||||
def mobile_user(mobile):
|
||||
try:
|
||||
print(username)
|
||||
user = CUser.objects.get(username=username)
|
||||
|
||||
if user and user.check_password(password):
|
||||
|
||||
cuser_roles = CuserRoleModel.objects.filter(cuser=user)
|
||||
CUserRoleLoginOpt.objects.filter(cuser_role__cuser=user).delete()
|
||||
|
||||
if cuser_roles:
|
||||
for user_role_obj in cuser_roles:
|
||||
|
||||
if not (user_role_obj.is_active and user_role_obj.mobile_verify):
|
||||
raise exceptions.ValidationError({"error": 'Not a valid user'})
|
||||
|
||||
expire_datetime = timezone.datetime.now() + timezone.timedelta(minutes=5)
|
||||
|
||||
user_role_opt = CUserRoleLoginOpt()
|
||||
user_role_opt.cuser_role = user_role_obj
|
||||
user_role_opt.cuser = user_role_obj.cuser
|
||||
user_role_opt.expire = expire_datetime
|
||||
user_role_opt.uuid = uuid.uuid4()
|
||||
user_role_opt.save()
|
||||
|
||||
login_opts = CUserRoleLoginOpt.objects.filter(cuser_role__cuser=user)
|
||||
return login_opts
|
||||
|
||||
else:
|
||||
raise exceptions.ValidationError({'error': "User not have role"})
|
||||
else:
|
||||
raise exceptions.ValidationError({'error': "Username or password is not correct"})
|
||||
|
||||
except CUser.DoesNotExist:
|
||||
raise exceptions.ValidationError({'error': "Username or password is not correct"})
|
||||
mobile = normalize_mobile(mobile)
|
||||
except DjangoValidationError as error:
|
||||
raise exceptions.ValidationError(error.message_dict)
|
||||
user = CUser.objects.filter(mobile=mobile).first()
|
||||
if not user:
|
||||
raise exceptions.ValidationError({'mobile': 'کاربری با این شماره موبایل ثبت نشده است.'})
|
||||
if not user.is_active:
|
||||
raise exceptions.PermissionDenied('حساب کاربری شما غیرفعال است.')
|
||||
roles = eligible_roles(user, customer_only=True)
|
||||
if not roles:
|
||||
raise exceptions.PermissionDenied('دسترسی شما به پنل مشتریان فعال نیست.')
|
||||
return user, roles
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def login_by_mobile(mobile: int):
|
||||
try:
|
||||
|
||||
exists_cuser = CUser.objects.get(mobile=mobile)
|
||||
|
||||
random_number = random.randint(100000, 999999)
|
||||
user, _ = mobile_user(mobile)
|
||||
# Serialize code requests for this account, retaining the old valid code if
|
||||
# the provider refuses the new message.
|
||||
CUser.objects.select_for_update().get(pk=user.pk)
|
||||
code = secrets.randbelow(900000) + 100000
|
||||
if not DefaultSmsProvider().send_sms(str(user.mobile), f'کد ورود شما: {code}'):
|
||||
raise SmsDeliveryUnavailable()
|
||||
SmsOpt.objects.filter(mobile=user.mobile).delete()
|
||||
SmsOpt.objects.create(mobile=user.mobile, opt=code, expire=timezone.now() + timedelta(minutes=3))
|
||||
return user.mobile
|
||||
|
||||
|
||||
try:
|
||||
SmsOptModel.objects.filter(mobile=mobile).delete()
|
||||
except Exception as exp:
|
||||
pass
|
||||
|
||||
expire_time = timezone.now() + timedelta(minutes=3)
|
||||
opt_code = SmsOptModel(mobile=mobile, opt=random_number, expire=expire_time)
|
||||
opt_code.save()
|
||||
|
||||
sms_text = f' کد ورود شما: {random_number}'
|
||||
|
||||
payload = {
|
||||
'USERNAME': 'vsms7937',
|
||||
'PASSWORD': '335577775533',
|
||||
'FROM': '30005367',
|
||||
'TO': int(mobile),
|
||||
'TEXT': f'{sms_text}'
|
||||
}
|
||||
r = requests.get('http://www.0098sms.com/sendsmslink.aspx', params=payload)
|
||||
|
||||
except Exception as exp:
|
||||
raise exceptions.ValidationError({'error': "mobile not found"})
|
||||
|
||||
|
||||
def verify_mobile_and_get_token(mobile: int, opt: int) -> CUserRoleLoginOpt:
|
||||
try:
|
||||
time_now = timezone.now()
|
||||
opt_obj = SmsOptModel.objects.filter(mobile=mobile, opt=opt, expire__gte=time_now)[0]
|
||||
|
||||
if opt_obj:
|
||||
cuser_roles = CuserRoleModel.objects.filter(cuser__mobile=mobile, is_active=True)
|
||||
CUserRoleLoginOpt.objects.filter(cuser_role__in=cuser_roles).delete()
|
||||
|
||||
if cuser_roles:
|
||||
for user_role_obj in cuser_roles:
|
||||
expire_datetime = timezone.datetime.now() + timezone.timedelta(minutes=5)
|
||||
|
||||
user_role_opt = CUserRoleLoginOpt()
|
||||
user_role_opt.cuser_role = user_role_obj
|
||||
user_role_opt.cuser = user_role_obj.cuser
|
||||
user_role_opt.expire = expire_datetime
|
||||
user_role_opt.uuid = uuid.uuid4()
|
||||
user_role_opt.save()
|
||||
|
||||
login_opts = CUserRoleLoginOpt.objects.filter(cuser_role__in=cuser_roles)
|
||||
SmsOptModel.objects.filter(mobile=mobile).delete()
|
||||
return login_opts
|
||||
except Exception as exp:
|
||||
try:
|
||||
SmsOptModel.objects.filter(mobile=mobile).delete()
|
||||
except Exception as exp:
|
||||
pass
|
||||
raise exceptions.ValidationError({'error': "cannot verify this mobile and opt code:"+str(exp)})
|
||||
@transaction.atomic
|
||||
def verify_mobile_and_get_token(mobile: int, opt: int):
|
||||
user, roles = mobile_user(mobile)
|
||||
CUser.objects.select_for_update().get(pk=user.pk)
|
||||
code = SmsOpt.objects.select_for_update().filter(
|
||||
mobile=user.mobile, opt=opt, expire__gt=timezone.now(),
|
||||
).first()
|
||||
if not code:
|
||||
raise exceptions.ValidationError({'opt': 'کد تأیید اشتباه است یا اعتبار آن تمام شده است.'})
|
||||
options = create_login_options(user, roles)
|
||||
SmsOpt.objects.filter(mobile=user.mobile).delete()
|
||||
return options
|
||||
@@ -0,0 +1,49 @@
|
||||
from datetime import datetime, timezone as datetime_timezone
|
||||
|
||||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from apps.jwt_auth.models import JwtToken
|
||||
|
||||
|
||||
def validate_role_access(role):
|
||||
if not (role.cuser and role.cuser.is_active and role.is_active and role.mobile_verify):
|
||||
raise AuthenticationFailed('حساب کاربری یا نقش شما غیرفعال است. دوباره وارد شوید.', code='session_invalid')
|
||||
workspace = role.sys_ws
|
||||
if not workspace or not workspace.is_active or (workspace.expire and workspace.expire <= timezone.now()):
|
||||
raise AuthenticationFailed('دسترسی فضای کاری غیرفعال یا منقضی شده است.', code='session_invalid')
|
||||
if role.role_type.role_name_en == 'workspace_customer' and role.customer_panel is not True:
|
||||
raise AuthenticationFailed('دسترسی شما به پنل مشتریان فعال نیست.', code='session_invalid')
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def issue_tokens(role, session=None):
|
||||
validate_role_access(role)
|
||||
if session is None:
|
||||
session = JwtToken.objects.create(cuser=role.cuser, cuser_role=role, refresh='')
|
||||
refresh = RefreshToken.for_user(role.cuser)
|
||||
refresh['workspace_uuid'] = str(role.sys_ws.uuid)
|
||||
refresh['cuser_role_uuid'] = str(role.uuid)
|
||||
refresh['user_uuid'] = str(role.cuser.uuid)
|
||||
refresh['session_id'] = session.pk
|
||||
data = {'refresh': str(refresh), 'access': str(refresh.access_token)}
|
||||
session.refresh = data['refresh']
|
||||
session.access = data['access']
|
||||
session.expire = datetime.fromtimestamp(refresh['exp'], tz=datetime_timezone.utc)
|
||||
session.save()
|
||||
return data
|
||||
|
||||
|
||||
def validate_session_claims(session, token):
|
||||
if session.expire and session.expire <= timezone.now():
|
||||
raise AuthenticationFailed('نشست شما منقضی شده است. دوباره وارد شوید.', code='session_invalid')
|
||||
role = session.cuser_role
|
||||
validate_role_access(role)
|
||||
expected = {'user_id': session.cuser_id, 'user_uuid': str(role.cuser.uuid),
|
||||
'cuser_role_uuid': str(role.uuid), 'workspace_uuid': str(role.sys_ws.uuid)}
|
||||
if token.get('session_id') is not None:
|
||||
expected['session_id'] = session.pk
|
||||
if role.cuser_id != session.cuser_id or any(token.get(key) != value for key, value in expected.items()):
|
||||
raise AuthenticationFailed('نشست شما معتبر نیست. دوباره وارد شوید.', code='session_invalid')
|
||||
@@ -0,0 +1,453 @@
|
||||
"""Authentication, duplicate mobiles and ordering integration regressions."""
|
||||
from datetime import timedelta
|
||||
from io import BytesIO
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.contrib import admin
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.utils import timezone
|
||||
from openpyxl import load_workbook
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from apps.cuser.cuser_model import CUser
|
||||
from apps.cuser.models import CuserRole, RoleType, SmsOpt
|
||||
from apps.customer.models import Customer, ResponsibleGroup, ShiveAshenaei
|
||||
from apps.jwt_auth.models import CUserRoleLoginOpt, JwtToken
|
||||
from apps.jwt_auth.session import issue_tokens
|
||||
from apps.shop.admin import ProductAdmin, ProductCategoryAdmin, excel_response
|
||||
from apps.shop.models import Product, ProductCategory, Order, OrderItem
|
||||
from apps.workspace.models import City, Workspace
|
||||
from utils.services.sms import DosefrNavadHashtSMS
|
||||
from utils.validations.mobile import normalize_mobile
|
||||
|
||||
|
||||
class RegressionFixtures(APITestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.workspace = Workspace.objects.create(name='Test', username='test')
|
||||
cls.other_workspace = Workspace.objects.create(name='Other', username='other')
|
||||
cls.user = CUser.objects.create_user('client', 'password', mobile=9123456789, name_family='Client', is_active=True)
|
||||
cls.customer_type = RoleType.objects.create(role_name='Customer', role_name_en='workspace_customer')
|
||||
cls.operator_type = RoleType.objects.create(role_name='Operator', role_name_en='workspace_operator')
|
||||
cls.admin_type = RoleType.objects.create(role_name='Admin', role_name_en='workspace_admin')
|
||||
cls.role = CuserRole.objects.create(cuser=cls.user, role_type=cls.customer_type, sys_ws=cls.workspace,
|
||||
name_family='Client', mobile=cls.user.mobile, is_active=True,
|
||||
mobile_verify=True, customer_panel=True)
|
||||
cls.operator = CuserRole.objects.create(cuser=cls.user, role_type=cls.operator_type, sys_ws=cls.workspace,
|
||||
name_family='Operator', mobile=cls.user.mobile, is_active=True, mobile_verify=True)
|
||||
cls.customer = Customer.objects.create(name_family='Client', mobile=cls.user.mobile, cuser_role=cls.role, sys_ws=cls.workspace)
|
||||
cls.city = City.objects.create(name='City')
|
||||
cls.group = ResponsibleGroup.objects.create(name='Group', sys_ws=cls.workspace)
|
||||
cls.shive = ShiveAshenaei.objects.create(name='Web', sys_ws=cls.workspace)
|
||||
|
||||
def use_role(self, role=None):
|
||||
tokens = issue_tokens(role or self.role)
|
||||
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + tokens['access'])
|
||||
return tokens
|
||||
|
||||
def post(self, path, data):
|
||||
return self.client.post('/auth/' + path, data, format='json')
|
||||
|
||||
def otp(self, expired=False):
|
||||
return SmsOpt.objects.create(mobile=self.user.mobile, opt=123456,
|
||||
expire=timezone.now() + timedelta(minutes=-1 if expired else 3))
|
||||
|
||||
|
||||
class AuthRegressionTests(RegressionFixtures):
|
||||
def test_unknown_mobile_has_useful_400(self):
|
||||
response = self.post('login/mobile/', {'mobile': '09999999999'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn('ثبت نشده', str(response.data))
|
||||
|
||||
def test_invalid_mobile_has_400(self):
|
||||
self.assertEqual(self.post('login/mobile/', {'mobile': '123'}).status_code, 400)
|
||||
|
||||
def test_disabled_customer_panel_has_403_and_sends_no_sms(self):
|
||||
self.role.customer_panel = False
|
||||
self.role.save()
|
||||
with patch('apps.jwt_auth.services.DefaultSmsProvider.send_sms') as send:
|
||||
response = self.post('login/mobile/', {'mobile': self.user.mobile})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertIn('پنل مشتریان', str(response.data))
|
||||
send.assert_not_called()
|
||||
|
||||
def test_inactive_user_has_403(self):
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
self.assertEqual(self.post('login/mobile/', {'mobile': self.user.mobile}).status_code, 403)
|
||||
|
||||
def test_sms_failure_has_503_and_preserves_old_otp(self):
|
||||
old = self.otp()
|
||||
with patch('apps.jwt_auth.services.DefaultSmsProvider.send_sms', return_value=False):
|
||||
response = self.post('login/mobile/', {'mobile': self.user.mobile})
|
||||
self.assertEqual(response.status_code, 503)
|
||||
self.assertIn('ارسال پیامک', str(response.data))
|
||||
self.assertTrue(SmsOpt.objects.filter(pk=old.pk).exists())
|
||||
|
||||
def test_successful_sms_saves_only_one_code(self):
|
||||
self.otp()
|
||||
with patch('apps.jwt_auth.services.DefaultSmsProvider.send_sms', return_value=True):
|
||||
response = self.post('login/mobile/', {'mobile': '+98 912-345-6789'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data['result']['mobile'], self.user.mobile)
|
||||
self.assertEqual(SmsOpt.objects.count(), 1)
|
||||
self.assertGreaterEqual(SmsOpt.objects.get().opt, 100000)
|
||||
|
||||
def test_wrong_otp_preserves_valid_code(self):
|
||||
old = self.otp()
|
||||
self.assertEqual(self.post('login/mobile/verify/', {'mobile': self.user.mobile, 'opt': 654321}).status_code, 400)
|
||||
self.assertTrue(SmsOpt.objects.filter(pk=old.pk).exists())
|
||||
|
||||
def test_expired_otp_rejected(self):
|
||||
self.otp(expired=True)
|
||||
self.assertEqual(self.post('login/mobile/verify/', {'mobile': self.user.mobile, 'opt': 123456}).status_code, 400)
|
||||
|
||||
def test_otp_is_single_use_and_roles_have_distinct_options(self):
|
||||
CuserRole.objects.create(cuser=self.user, role_type=self.customer_type, sys_ws=self.other_workspace,
|
||||
name_family='Other', mobile=self.user.mobile, is_active=True, mobile_verify=True, customer_panel=True)
|
||||
self.otp()
|
||||
response = self.post('login/mobile/verify/', {'mobile': self.user.mobile, 'opt': 123456})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response.data), 2)
|
||||
self.assertNotEqual(response.data[0]['uuid'], response.data[1]['uuid'])
|
||||
self.assertEqual(self.post('login/mobile/verify/', {'mobile': self.user.mobile, 'opt': 123456}).status_code, 400)
|
||||
|
||||
def test_bad_password_has_400(self):
|
||||
self.assertEqual(self.post('login/username/', {'username': 'client', 'password': 'wrong'}).status_code, 400)
|
||||
|
||||
def test_public_login_ignores_stale_authorization(self):
|
||||
self.client.credentials(HTTP_AUTHORIZATION='Bearer invalid')
|
||||
response = self.post('login/username/', {'username': 'client', 'password': 'password'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response.data), 2)
|
||||
|
||||
def test_option_expiry_and_replay_have_401(self):
|
||||
option = CUserRoleLoginOpt.objects.create(cuser=self.user, cuser_role=self.role, expire=timezone.now()-timedelta(seconds=1))
|
||||
self.assertEqual(self.post('token/', {'uuid_opt': str(option.uuid)}).status_code, 401)
|
||||
option.expire = timezone.now()+timedelta(minutes=5)
|
||||
option.save()
|
||||
result = self.post('token/', {'uuid_opt': str(option.uuid)})
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(self.post('token/', {'uuid_opt': str(option.uuid)}).status_code, 401)
|
||||
self.assertEqual(JwtToken.objects.count(), 1)
|
||||
|
||||
def test_anonymous_info_has_401(self):
|
||||
self.assertEqual(self.client.get('/cuser/info/').status_code, 401)
|
||||
|
||||
def test_signed_but_unrecorded_access_is_rejected(self):
|
||||
refresh = RefreshToken.for_user(self.user)
|
||||
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + str(refresh.access_token))
|
||||
self.assertEqual(self.client.get('/cuser/info/').status_code, 401)
|
||||
|
||||
def test_blocked_access_and_refresh_are_rejected(self):
|
||||
tokens = self.use_role()
|
||||
JwtToken.objects.update(is_blocked=True)
|
||||
self.assertEqual(self.client.get('/cuser/info/').status_code, 401)
|
||||
self.assertEqual(self.post('token/refresh/', {'refresh': tokens['refresh']}).status_code, 401)
|
||||
|
||||
def test_missing_session_rejects_access_and_refresh(self):
|
||||
tokens = self.use_role()
|
||||
JwtToken.objects.all().delete()
|
||||
self.assertEqual(self.client.get('/cuser/info/').status_code, 401)
|
||||
self.assertEqual(self.post('token/refresh/', {'refresh': tokens['refresh']}).status_code, 401)
|
||||
|
||||
def test_refresh_rotates_and_retains_workspace(self):
|
||||
tokens = self.use_role(self.operator)
|
||||
response = self.post('token/refresh/', {'refresh': tokens['refresh']})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotEqual(response.data['refresh'], tokens['refresh'])
|
||||
self.assertEqual(RefreshToken(response.data['refresh'])['workspace_uuid'], str(self.workspace.uuid))
|
||||
self.assertEqual(JwtToken.objects.count(), 1)
|
||||
self.assertGreater(JwtToken.objects.get().expire, timezone.now())
|
||||
self.assertEqual(self.post('token/refresh/', {'refresh': tokens['refresh']}).status_code, 401)
|
||||
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + response.data['access'])
|
||||
self.assertEqual(self.client.get('/cuser/info/').status_code, 200)
|
||||
|
||||
def test_disabling_panel_revokes_both_tokens(self):
|
||||
tokens = self.use_role()
|
||||
self.role.customer_panel = False
|
||||
self.role.save()
|
||||
self.assertEqual(self.client.get('/cuser/info/').status_code, 401)
|
||||
self.assertEqual(self.post('token/refresh/', {'refresh': tokens['refresh']}).status_code, 401)
|
||||
|
||||
def test_expired_workspace_revokes_both_tokens(self):
|
||||
tokens = self.use_role()
|
||||
self.workspace.expire = timezone.now()-timedelta(seconds=1)
|
||||
self.workspace.save()
|
||||
self.assertEqual(self.client.get('/cuser/info/').status_code, 401)
|
||||
self.assertEqual(self.post('token/refresh/', {'refresh': tokens['refresh']}).status_code, 401)
|
||||
|
||||
def test_logout_revokes_session_without_valid_access(self):
|
||||
tokens = self.use_role()
|
||||
self.client.credentials(HTTP_AUTHORIZATION='Bearer expired')
|
||||
self.assertEqual(self.post('logout/', {'refresh': tokens['refresh']}).status_code, 204)
|
||||
self.assertEqual(self.post('token/refresh/', {'refresh': tokens['refresh']}).status_code, 401)
|
||||
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + tokens['access'])
|
||||
self.assertEqual(self.client.get('/cuser/info/').status_code, 401)
|
||||
self.assertEqual(self.post('logout/', {'refresh': tokens['refresh']}).status_code, 204)
|
||||
|
||||
def test_user_duplicate_formats_are_rejected(self):
|
||||
for mobile in ['09123456789', '+98 912-345-6789', '۰۹۱۲۳۴۵۶۷۸۹', '00989123456789']:
|
||||
with self.subTest(mobile=mobile), self.assertRaises(ValidationError):
|
||||
CUser.objects.create_user('duplicate', 'password', mobile=mobile)
|
||||
self.assertEqual(CUser.objects.count(), 1)
|
||||
|
||||
def test_customer_duplicate_has_database_constraint(self):
|
||||
with self.assertRaises(IntegrityError), transaction.atomic():
|
||||
Customer.objects.create(name_family='Duplicate', mobile=self.user.mobile, sys_ws=self.workspace)
|
||||
Customer.objects.create(name_family='Other', mobile=self.user.mobile, sys_ws=self.other_workspace)
|
||||
Customer.objects.create(name_family='Null1', sys_ws=self.workspace)
|
||||
Customer.objects.create(name_family='Null2', sys_ws=self.workspace)
|
||||
|
||||
def test_create_customer_v2_duplicate_has_mobile_error(self):
|
||||
self.use_role(self.operator)
|
||||
response = self.client.post('/customer/v2/create/', {
|
||||
'name_family': 'Duplicate', 'mobile': '+98 912-345-6789', 'phone': 2133333333,
|
||||
'address': 'Address', 'city': self.city.pk, 'shive_ashenaei': self.shive.pk, 'responsible_group': self.group.pk,
|
||||
}, format='json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn('mobile', response.data)
|
||||
self.assertEqual(Customer.objects.count(), 1)
|
||||
|
||||
def test_partial_customer_update_rejects_duplicate_and_allows_same_mobile(self):
|
||||
other = Customer.objects.create(name_family='Other', mobile=9333333333, sys_ws=self.workspace)
|
||||
self.use_role(self.operator)
|
||||
response = self.client.patch(f'/customer/update/{other.pk}/', {'mobile': '09123456789'}, format='json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn('mobile', response.data)
|
||||
response = self.client.patch(f'/customer/update/{self.customer.pk}/', {'mobile': self.user.mobile}, format='json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_panel_link_is_scoped_to_workspace(self):
|
||||
other_operator = CuserRole.objects.create(cuser=self.user, role_type=self.operator_type, sys_ws=self.other_workspace,
|
||||
name_family='Operator2', mobile=self.user.mobile, is_active=True, mobile_verify=True)
|
||||
other_customer = Customer.objects.create(name_family='Other', mobile=self.user.mobile, sys_ws=self.other_workspace)
|
||||
self.use_role(other_operator)
|
||||
response = self.client.post('/customer/customer-panel/', {'customer_id': other_customer.pk, 'mobile': self.user.mobile, 'status': True}, format='json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
other_customer.refresh_from_db()
|
||||
self.assertNotEqual(other_customer.cuser_role_id, self.role.pk)
|
||||
self.assertEqual(other_customer.cuser_role.sys_ws_id, self.other_workspace.pk)
|
||||
|
||||
def test_disable_panel_without_link_does_not_create_user(self):
|
||||
other = Customer.objects.create(name_family='Other', mobile=9333333333, sys_ws=self.workspace)
|
||||
self.use_role(self.operator)
|
||||
response = self.client.post('/customer/customer-panel/', {'customer_id': other.pk, 'mobile': other.mobile, 'status': False}, format='json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNone(response.data['cuser_role_id'])
|
||||
self.assertEqual(CUser.objects.count(), 1)
|
||||
|
||||
def test_provider_checks_body_and_handles_timeout(self):
|
||||
import requests
|
||||
with patch('utils.services.sms.requests.get', return_value=Mock(text='0')) as send:
|
||||
self.assertTrue(DosefrNavadHashtSMS().send_sms(str(self.user.mobile), 'Test'))
|
||||
self.assertEqual(send.call_args.kwargs['timeout'], 10)
|
||||
with patch('utils.services.sms.requests.get', return_value=Mock(text='error')):
|
||||
self.assertFalse(DosefrNavadHashtSMS().send_sms(str(self.user.mobile), 'Test'))
|
||||
with patch('utils.services.sms.requests.get', side_effect=requests.Timeout):
|
||||
self.assertFalse(DosefrNavadHashtSMS().send_sms(str(self.user.mobile), 'Test'))
|
||||
|
||||
def test_iranian_mobile_prefix_is_not_accidentally_removed(self):
|
||||
self.assertEqual(normalize_mobile('9891234567'), 9891234567)
|
||||
|
||||
def test_logout_during_refresh_rotation_revokes_new_tokens(self):
|
||||
tokens = self.use_role()
|
||||
rotated = self.post('token/refresh/', {'refresh': tokens['refresh']})
|
||||
self.assertEqual(rotated.status_code, 200)
|
||||
self.assertEqual(self.post('logout/', {'refresh': tokens['refresh']}).status_code, 204)
|
||||
self.assertEqual(self.post('token/refresh/', {'refresh': rotated.data['refresh']}).status_code, 401)
|
||||
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + rotated.data['access'])
|
||||
self.assertEqual(self.client.get('/cuser/info/').status_code, 401)
|
||||
|
||||
def test_mobile_change_disconnects_old_panel_and_revokes_old_session(self):
|
||||
tokens = issue_tokens(self.role)
|
||||
self.use_role(self.operator)
|
||||
response = self.client.patch(f'/customer/update/{self.customer.pk}/', {'mobile': '09333333333'}, format='json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.customer.refresh_from_db()
|
||||
self.role.refresh_from_db()
|
||||
self.assertIsNone(self.customer.cuser_role_id)
|
||||
self.assertFalse(self.role.customer_panel)
|
||||
self.assertEqual(self.post('token/refresh/', {'refresh': tokens['refresh']}).status_code, 401)
|
||||
|
||||
def test_duplicate_migration_audits_before_changing_any_data(self):
|
||||
import importlib
|
||||
from django.apps import apps
|
||||
from django.db import connection
|
||||
duplicate = Customer.objects.create(name_family='Legacy', mobile=989123456789, sys_ws=self.workspace)
|
||||
migration = importlib.import_module('apps.customer.migrations.0008_customer_mobile_unique')
|
||||
with self.assertRaises(RuntimeError):
|
||||
migration.check_and_normalize_mobiles(apps, Mock(connection=connection))
|
||||
duplicate.refresh_from_db()
|
||||
self.assertEqual(duplicate.mobile, 989123456789)
|
||||
|
||||
def test_migration_normalizes_legacy_country_prefix(self):
|
||||
import importlib
|
||||
from django.apps import apps
|
||||
from django.db import connection
|
||||
legacy = Customer.objects.create(name_family='Legacy', mobile=989333333333, sys_ws=self.workspace)
|
||||
migration = importlib.import_module('apps.customer.migrations.0008_customer_mobile_unique')
|
||||
migration.check_and_normalize_mobiles(apps, Mock(connection=connection))
|
||||
legacy.refresh_from_db()
|
||||
self.assertEqual(legacy.mobile, 9333333333)
|
||||
|
||||
def test_customer_excel_import_reports_duplicate_and_rolls_back_all_rows(self):
|
||||
import pandas as pd
|
||||
self.city.ostan = City.objects.create(name='Province', is_ostan=True)
|
||||
self.city.save()
|
||||
role = CuserRole.objects.create(cuser=self.user, role_type=self.admin_type, sys_ws=self.workspace,
|
||||
name_family='Admin', mobile=self.user.mobile, is_active=True, mobile_verify=True)
|
||||
self.use_role(role)
|
||||
rows = [{'name': 'New', 'mobile': 9333333333, 'phone': None, 'address': None,
|
||||
'ostan': 'Province', 'shar': 'City', 'id_mantaghe': self.group.pk},
|
||||
{'name': 'Duplicate', 'mobile': self.user.mobile, 'phone': None, 'address': None,
|
||||
'ostan': 'Province', 'shar': 'City', 'id_mantaghe': self.group.pk}]
|
||||
with patch('apps.shop.views.excel_import.pd.read_excel', return_value=pd.DataFrame(rows)):
|
||||
response = self.client.get('/shop/import/customer/')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn('mobile', response.data['errors'])
|
||||
self.assertEqual(Customer.objects.count(), 1)
|
||||
|
||||
def test_anonymous_import_requests_are_rejected(self):
|
||||
self.assertEqual(self.client.get('/shop/import/customer/').status_code, 401)
|
||||
self.assertEqual(self.client.get('/shop/import/').status_code, 401)
|
||||
|
||||
def test_recorded_session_expiry_is_enforced(self):
|
||||
tokens = self.use_role()
|
||||
JwtToken.objects.update(expire=timezone.now()-timedelta(seconds=1))
|
||||
self.assertEqual(self.client.get('/cuser/info/').status_code, 401)
|
||||
self.assertEqual(self.post('token/refresh/', {'refresh': tokens['refresh']}).status_code, 401)
|
||||
|
||||
def test_inactive_role_and_workspace_reject_access(self):
|
||||
self.use_role()
|
||||
self.role.is_active = False
|
||||
self.role.save()
|
||||
self.assertEqual(self.client.get('/cuser/info/').status_code, 401)
|
||||
self.role.is_active = True
|
||||
self.role.save()
|
||||
self.workspace.is_active = False
|
||||
self.workspace.save()
|
||||
self.assertEqual(self.client.get('/cuser/info/').status_code, 401)
|
||||
|
||||
|
||||
def test_admin_user_creation_hashes_password_and_normalizes_mobile(self):
|
||||
self.user.is_staff = True
|
||||
self.user.is_superuser = True
|
||||
self.user.save()
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post('/admin/cuser/cuser/add/', {
|
||||
'username': 'new-user', 'name_family': 'New User', 'mobile': '+98 933-333-3333',
|
||||
'password1': 'Valid-Password-9320!', 'password2': 'Valid-Password-9320!',
|
||||
'is_active': 'on', 'is_staff': 'on', '_save': 'Save',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
user = CUser.objects.get(username='new-user')
|
||||
self.assertEqual(user.mobile, 9333333333)
|
||||
self.assertTrue(user.check_password('Valid-Password-9320!'))
|
||||
self.assertNotEqual(user.password, 'Valid-Password-9320!')
|
||||
|
||||
def test_admin_duplicate_user_mobile_is_a_form_error(self):
|
||||
self.user.is_staff = True
|
||||
self.user.is_superuser = True
|
||||
self.user.save()
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post('/admin/cuser/cuser/add/', {
|
||||
'username': 'duplicate', 'mobile': '+98 912-345-6789',
|
||||
'password1': 'Valid-Password-9320!', 'password2': 'Valid-Password-9320!', '_save': 'Save',
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('mobile', response.context['adminform'].form.errors)
|
||||
self.assertEqual(CUser.objects.count(), 1)
|
||||
|
||||
|
||||
class OrderingRegressionTests(RegressionFixtures):
|
||||
def setUp(self):
|
||||
self.first = ProductCategory.objects.create(name='First', sys_ws=self.workspace, order_cat=1, excel_order=20)
|
||||
self.second = ProductCategory.objects.create(name='Second', sys_ws=self.workspace, order_cat=2, excel_order=10)
|
||||
self.unordered = ProductCategory.objects.create(name='Unordered', sys_ws=self.workspace)
|
||||
self.a = Product.objects.create(name='A', category=self.first, sys_ws=self.workspace, price=10, order_prd=2, excel_order=1, attr={'acc_vahed': 'Unit'})
|
||||
self.b = Product.objects.create(name='B', category=self.first, sys_ws=self.workspace, price=20, order_prd=1, excel_order=2)
|
||||
self.c = Product.objects.create(name='C', category=self.second, sys_ws=self.workspace, price=30, order_prd=1)
|
||||
|
||||
def test_display_order_groups_categories_and_products(self):
|
||||
self.assertEqual(list(ProductCategory.objects.values_list('name', flat=True)), ['First', 'Second', 'Unordered'])
|
||||
self.assertEqual(list(Product.objects.values_list('name', flat=True)), ['B', 'A', 'C'])
|
||||
|
||||
def test_admin_excel_independent_order_and_current_column_order(self):
|
||||
model_admin = ProductAdmin(Product, admin.site)
|
||||
sheet = load_workbook(BytesIO(model_admin.export_excel_order(None, Product.objects.all()).content)).active
|
||||
self.assertEqual([row[2] for row in list(sheet.values)[1:]], ['C', 'A', 'B'])
|
||||
sheet = load_workbook(BytesIO(model_admin.export_display_order(None, Product.objects.order_by('-name')).content)).active
|
||||
self.assertEqual([row[2] for row in list(sheet.values)[1:]], ['C', 'B', 'A'])
|
||||
category_admin = ProductCategoryAdmin(ProductCategory, admin.site)
|
||||
sheet = load_workbook(BytesIO(category_admin.export_excel_order(None, ProductCategory.objects.all()).content)).active
|
||||
self.assertEqual([row[1] for row in list(sheet.values)[1:]], ['Second', 'First', 'Unordered'])
|
||||
|
||||
def test_empty_excel_order_falls_back_to_display(self):
|
||||
ProductCategory.objects.update(excel_order=None)
|
||||
Product.objects.update(excel_order=None)
|
||||
response = ProductAdmin(Product, admin.site).export_excel_order(None, Product.objects.all())
|
||||
sheet = load_workbook(BytesIO(response.content)).active
|
||||
self.assertEqual([row[2] for row in list(sheet.values)[1:]], ['B', 'A', 'C'])
|
||||
|
||||
def test_excel_keeps_formula_like_names_as_text(self):
|
||||
response = excel_response(('Name',), [('=1+1',)], 'test.xlsx')
|
||||
cell = load_workbook(BytesIO(response.content)).active['A2']
|
||||
self.assertEqual(cell.data_type, 's')
|
||||
self.assertEqual(cell.value, '=1+1')
|
||||
|
||||
def test_category_api_is_ordered_and_does_not_leak_other_workspace(self):
|
||||
foreign = ProductCategory.objects.create(name='Foreign', sys_ws=self.other_workspace, order_cat=0)
|
||||
Product.objects.create(name='Foreign product', sys_ws=self.other_workspace, category=foreign, price=1)
|
||||
self.use_role()
|
||||
response = self.client.get('/shop/v2/product-category/list/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual([row['name'] for row in response.data], ['First', 'Second'])
|
||||
response = self.client.get(f'/shop/product/list/{self.first.pk}/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNone(response.data['category_img'])
|
||||
self.assertEqual([row['name'] for row in response.data['products']], ['B', 'A'])
|
||||
|
||||
def test_order_excel_matches_independent_order_and_handles_empty_order(self):
|
||||
order = Order.objects.create(customer=self.customer, total_price=60, factor_type=1, sys_ws=self.workspace)
|
||||
for product in [self.b, self.a, self.c]:
|
||||
OrderItem.objects.create(order=order, product=product, product_name=product.name, price=product.price, amount=1)
|
||||
self.use_role(self.operator)
|
||||
response = self.client.get(f'/shop/order/{order.pk}/operator/excel/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
sheet = load_workbook(BytesIO(response.content)).active
|
||||
self.assertEqual([row[0] for row in list(sheet.values)[1:]], ['SecondC', 'FirstA', 'FirstB'])
|
||||
OrderItem.objects.all().delete()
|
||||
response = self.client.get(f'/shop/order/{order.pk}/operator/excel/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(load_workbook(BytesIO(response.content)).active.max_row, 1)
|
||||
|
||||
def test_order_excel_requires_operator_and_matching_workspace(self):
|
||||
order = Order.objects.create(customer=self.customer, total_price=0, factor_type=1, sys_ws=self.other_workspace)
|
||||
self.use_role()
|
||||
self.assertEqual(self.client.get(f'/shop/order/{order.pk}/operator/excel/').status_code, 403)
|
||||
self.use_role(self.operator)
|
||||
self.assertEqual(self.client.get(f'/shop/order/{order.pk}/operator/excel/').status_code, 400)
|
||||
|
||||
def test_admin_bulk_edit_category_and_product_orders(self):
|
||||
self.user.is_staff = True
|
||||
self.user.is_superuser = True
|
||||
self.user.save()
|
||||
self.client.force_login(self.user)
|
||||
for model, field, objects in [('productcategory', 'order_cat', [self.first, self.second, self.unordered]),
|
||||
('product', 'order_prd', [self.a, self.b, self.c])]:
|
||||
path = f'/admin/shop/{model}/'
|
||||
response = self.client.get(path)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = {'_save': 'Save', 'form-TOTAL_FORMS': len(objects), 'form-INITIAL_FORMS': len(objects),
|
||||
'form-MIN_NUM_FORMS': 0, 'form-MAX_NUM_FORMS': 1000}
|
||||
for index, obj in enumerate(objects):
|
||||
data.update({f'form-{index}-id': obj.pk, f'form-{index}-{field}': 10+index,
|
||||
f'form-{index}-excel_order': 20-index})
|
||||
self.assertEqual(self.client.post(path, data).status_code, 302)
|
||||
for index, obj in enumerate(objects):
|
||||
obj.refresh_from_db()
|
||||
self.assertEqual(getattr(obj, field), 10+index)
|
||||
self.assertEqual(obj.excel_order, 20-index)
|
||||
@@ -4,13 +4,15 @@ from apps.jwt_auth.views import (
|
||||
CustomTokenObtainPairView,
|
||||
LoginByUserView,
|
||||
LoginByMobileView,
|
||||
VerifyMobileAndGetTokenView
|
||||
VerifyMobileAndGetTokenView,
|
||||
LogoutView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('logout/', LogoutView.as_view(), name='logout'),
|
||||
path('login/username/', LoginByUserView.as_view(), name='login_by_username'),
|
||||
path('login/mobile/', LoginByMobileView.as_view(), name='login_by_mobile'),
|
||||
path('login/mobile/verify/', VerifyMobileAndGetTokenView.as_view(), name='verify_mobile'),
|
||||
path('token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
|
||||
path('token/refresh/', CustomTokenRefreshView.as_view(), name='token_refresh'),
|
||||
]
|
||||
]
|
||||
+107
-255
@@ -1,297 +1,149 @@
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import serializers
|
||||
from rest_framework.response import Response
|
||||
from django.db import transaction
|
||||
from apps.jwt_auth.models import JwtToken as JwtTokenModel, CUserRoleLoginOpt
|
||||
from apps.cuser.models import CuserRole as CuserRoleModel, RoleType as RoleTypeModel
|
||||
from apps.customer.models import Customer as CustomerModel, CustomerResponsible as CustomerResponsibleModel
|
||||
from rest_framework_simplejwt.tokens import RefreshToken, AccessToken, Token
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
from apps.workspace.models import Workspace as WorkspaceModel
|
||||
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 django.contrib.auth import get_user_model
|
||||
from rest_framework import status
|
||||
CUser = get_user_model()
|
||||
|
||||
# Create your views here.
|
||||
from apps.jwt_auth.session import issue_tokens, validate_session_claims
|
||||
from apps.workspace.models import Workspace
|
||||
|
||||
|
||||
class VerifyMobileAndGetTokenView(APIView):
|
||||
|
||||
class InputSerializer(serializers.Serializer):
|
||||
mobile = serializers.IntegerField(required=True)
|
||||
opt = serializers.IntegerField(required=True)
|
||||
class RoleTypeSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RoleType
|
||||
fields = ('role_name', 'role_name_en')
|
||||
|
||||
|
||||
class OutputSerializer(serializers.ModelSerializer):
|
||||
|
||||
class CUserRoleSerializer(serializers.ModelSerializer):
|
||||
|
||||
class RoleTypeSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RoleTypeModel
|
||||
fields = ('role_name', )
|
||||
|
||||
role_type = RoleTypeSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CuserRoleModel
|
||||
fields = ('name_family', 'title', 'role_type')
|
||||
|
||||
cuser_role = CUserRoleSerializer()
|
||||
|
||||
class RoleSerializer(serializers.ModelSerializer):
|
||||
class WorkspaceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CUserRoleLoginOpt
|
||||
fields = ('cuser_role', 'expire', 'uuid')
|
||||
model = Workspace
|
||||
fields = ('id', 'name')
|
||||
|
||||
def post(self, request):
|
||||
serializer = self.InputSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
role_type = RoleTypeSerializer()
|
||||
sys_ws = WorkspaceSerializer()
|
||||
|
||||
login_opts = verify_mobile_and_get_token(**serializer.validated_data)
|
||||
|
||||
response_data = self.OutputSerializer(login_opts, many=True).data
|
||||
|
||||
return Response(response_data)
|
||||
class Meta:
|
||||
model = CuserRole
|
||||
fields = ('name_family', 'title', 'role_type', 'sys_ws')
|
||||
|
||||
|
||||
class LoginByMobileView(APIView):
|
||||
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.IntegerField(required=True)
|
||||
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)
|
||||
|
||||
login_by_mobile(**serializer.validated_data)
|
||||
|
||||
output = {
|
||||
"result": {
|
||||
"mobile": serializer.validated_data.get('mobile'),
|
||||
"message": "opt code send"
|
||||
}
|
||||
}
|
||||
|
||||
return Response(output)
|
||||
options = verify_mobile_and_get_token(**serializer.validated_data)
|
||||
return Response(LoginOptionSerializer(options, many=True).data)
|
||||
|
||||
|
||||
class LoginByUserView(APIView):
|
||||
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)
|
||||
mobile = login_by_mobile(**serializer.validated_data)
|
||||
return Response({'result': {'mobile': mobile, 'message': 'opt code send'}})
|
||||
|
||||
|
||||
class LoginByUserView(PublicAuthView):
|
||||
class InputSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(required=True)
|
||||
password = serializers.CharField(required=True)
|
||||
|
||||
class OutputSerializer(serializers.ModelSerializer):
|
||||
|
||||
class CUserRoleSerializer(serializers.ModelSerializer):
|
||||
|
||||
class RoleTypeSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RoleTypeModel
|
||||
fields = ('role_name', )
|
||||
|
||||
role_type = RoleTypeSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CuserRoleModel
|
||||
fields = ('name_family', 'title', 'role_type')
|
||||
|
||||
cuser_role = CUserRoleSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CUserRoleLoginOpt
|
||||
fields = ('cuser_role', 'expire', 'uuid')
|
||||
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)
|
||||
|
||||
username = serializer.validated_data.get('username')
|
||||
password = serializer.validated_data.get('password')
|
||||
|
||||
login_opts = login_by_username(username=username, password=password)
|
||||
|
||||
response_data = self.OutputSerializer(login_opts, many=True).data
|
||||
|
||||
return Response(response_data)
|
||||
options = login_by_username(**serializer.validated_data)
|
||||
return Response(LoginOptionSerializer(options, many=True).data)
|
||||
|
||||
|
||||
class CustomTokenObtainPairView(APIView):
|
||||
|
||||
class CustomTokenObtainPairView(PublicAuthView):
|
||||
class InputSerializer(serializers.Serializer):
|
||||
uuid_opt = serializers.CharField(required=True)
|
||||
|
||||
class OutputSerializer(serializers.Serializer):
|
||||
access = serializers.CharField(required=True)
|
||||
refresh = serializers.CharField(required=True)
|
||||
uuid_opt = serializers.UUIDField(required=True)
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request):
|
||||
serializer = self.InputSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
uuid_opt = serializer.validated_data.get('uuid_opt')
|
||||
|
||||
try:
|
||||
user_login_opt = CUserRoleLoginOpt.objects.get(uuid=uuid_opt)
|
||||
user = user_login_opt.cuser
|
||||
cuser_role = user_login_opt.cuser_role
|
||||
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)
|
||||
|
||||
|
||||
workspace = cuser_role.sys_ws
|
||||
|
||||
if user_login_opt:
|
||||
refresh = RefreshToken.for_user(user)
|
||||
|
||||
#if 'user_id' in refresh:
|
||||
# del refresh['user_id']
|
||||
#print(cuser_role.uuid)
|
||||
|
||||
|
||||
'''try:
|
||||
#customerObj = CustomerModel.objects.get(cuser_role__uuid=str(cuser_role.uuid))
|
||||
# workspace = customerObj.sys_ws
|
||||
|
||||
workspace = cuser_role.sys_ws
|
||||
|
||||
except:
|
||||
operator = CustomerResponsibleModel.objects.get(cuser_role=cuser_role)
|
||||
workspace = operator.sys_ws'''
|
||||
|
||||
refresh['workspace_uuid'] = str(workspace.uuid)
|
||||
|
||||
refresh['cuser_role_uuid'] = str(cuser_role.uuid)
|
||||
|
||||
refresh['user_uuid'] = str(user.uuid)
|
||||
|
||||
#print(str(refresh))
|
||||
|
||||
try:
|
||||
jwt_db = JwtTokenModel(refresh=str(refresh), access=str(refresh.access_token),
|
||||
cuser=user, cuser_role=cuser_role)
|
||||
jwt_db.save()
|
||||
|
||||
try:
|
||||
CUserRoleLoginOpt.objects.filter(cuser=user).delete()
|
||||
except Exception as exp:
|
||||
pass
|
||||
|
||||
except Exception as exp:
|
||||
return Response({'error': str(exp)})
|
||||
|
||||
new_token_data = {
|
||||
'refresh': str(refresh),
|
||||
'access': str(refresh.access_token),
|
||||
}
|
||||
|
||||
out_data = self.OutputSerializer(data=new_token_data)
|
||||
out_data.is_valid(raise_exception=True)
|
||||
|
||||
return Response(out_data.data)
|
||||
else:
|
||||
return Response({"error": 'userpass is not True'})
|
||||
except CUserRoleLoginOpt.DoesNotExist:
|
||||
return Response({"error": 'Token not valid'})
|
||||
except Exception as exp:
|
||||
return Response({"error": 'user or pass is not true 2'})
|
||||
|
||||
|
||||
class CustomTokenRefreshView(APIView):
|
||||
|
||||
class CustomTokenRefreshView(PublicAuthView):
|
||||
class InputSerializer(serializers.Serializer):
|
||||
refresh = serializers.CharField(required=True)
|
||||
|
||||
class OutputSerializer(serializers.Serializer):
|
||||
access = 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)
|
||||
session = JwtToken.objects.select_for_update().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):
|
||||
with transaction.atomic():
|
||||
try:
|
||||
|
||||
serializer = self.InputSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
refresh_token = serializer.validated_data.get('refresh')
|
||||
|
||||
print(refresh_token)
|
||||
|
||||
# check: token is valid?
|
||||
ref_token = RefreshToken(token=refresh_token)
|
||||
|
||||
print("ref")
|
||||
|
||||
# first token must exists in whitelist and not blocked
|
||||
token_exist = JwtTokenModel.objects.get(refresh=str(refresh_token))
|
||||
|
||||
print("exsist")
|
||||
|
||||
token_is_blocked = JwtTokenModel.objects.get(refresh=str(refresh_token)).is_blocked
|
||||
|
||||
print("yess")
|
||||
|
||||
if token_exist and token_is_blocked is False:
|
||||
JwtTokenModel.objects.get(refresh=str(refresh_token)).delete()
|
||||
else:
|
||||
raise ValueError('refresh token is not in whitelist or blocked!')
|
||||
|
||||
user_uuid = ref_token.payload.get('user_uuid')
|
||||
user_id = ref_token.payload.get('user_id')
|
||||
cuser_role_uuid = ref_token.payload.get('cuser_role_uuid')
|
||||
|
||||
token_exp_timestamp = ref_token.payload.get('exp')
|
||||
token_exp = timezone.make_aware(timezone.datetime.fromtimestamp(token_exp_timestamp), timezone.utc)
|
||||
user = CUser.objects.get(id=user_id)
|
||||
cuser_role = CuserRoleModel.objects.get(uuid=cuser_role_uuid)
|
||||
|
||||
|
||||
print("OKH###")
|
||||
|
||||
if ref_token:
|
||||
new_token = RefreshToken.for_user(user)
|
||||
|
||||
#if 'user_id' in new_token:
|
||||
#del new_token['user_id']
|
||||
|
||||
new_token['user_uuid'] = str(user.uuid)
|
||||
new_token['cuser_role_uuid'] = str(cuser_role.uuid)
|
||||
|
||||
print("OKOKKO 555")
|
||||
|
||||
#workspace = WorkspaceModel.objects.get(cuser_role_owner__uuid=str(cuser_role.uuid))
|
||||
|
||||
try:
|
||||
customerObj = CustomerModel.objects.get(cuser_role__uuid=str(cuser_role.uuid))
|
||||
new_token['workspace_uuid'] = str(customerObj.sys_ws.uuid)
|
||||
except:
|
||||
operator = CustomerResponsibleModel.objects.get(cuser_role__uuid=str(cuser_role.uuid))
|
||||
new_token['workspace_uuid'] = str(operator.sys_ws.uuid)
|
||||
|
||||
|
||||
new_access_token = new_token.access_token
|
||||
|
||||
new_token_data = {
|
||||
'refresh': str(new_token),
|
||||
'access': str(new_access_token),
|
||||
}
|
||||
|
||||
print("OKOKKO")
|
||||
|
||||
new_token_obj = JwtTokenModel(refresh=str(new_token),
|
||||
access=str(new_token_data.get('access')),
|
||||
expire=token_exp,
|
||||
cuser=user,
|
||||
cuser_role=cuser_role
|
||||
)
|
||||
|
||||
print("OKOKKO 222")
|
||||
new_token_obj.save()
|
||||
out_data = self.OutputSerializer(data=new_token_data)
|
||||
out_data.is_valid(raise_exception=True)
|
||||
return Response(out_data.data)
|
||||
|
||||
except Exception as e:
|
||||
transaction.set_rollback(True)
|
||||
return Response({"error": str(e)})
|
||||
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)
|
||||
Reference in new issue
Block a user