diff --git a/apps/gooyal_oauth2/validators.py b/apps/gooyal_oauth2/validators.py index 3897d48..b4c930e 100755 --- a/apps/gooyal_oauth2/validators.py +++ b/apps/gooyal_oauth2/validators.py @@ -42,7 +42,7 @@ class OAuth2Validator(BaseOAuth2Validator): # pylint: disable=w0223 if user_field not in ['phone_number', 'username', 'email']: return False - if pass_field not in ['password', 'otp']: + if pass_field not in ['password', 'otp', 'ott']: return False if not username or not password: diff --git a/apps/users/migrations/0003_user_ott_user_ott_expire.py b/apps/users/migrations/0003_user_ott_user_ott_expire.py new file mode 100644 index 0000000..09ec203 --- /dev/null +++ b/apps/users/migrations/0003_user_ott_user_ott_expire.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1 on 2022-09-15 07:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_user_email_verified'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='ott', + field=models.CharField(blank=True, max_length=36, null=True, verbose_name='one time token'), + ), + migrations.AddField( + model_name='user', + name='ott_expire', + field=models.DateTimeField(blank=True, null=True, verbose_name='otp expire'), + ), + ] diff --git a/apps/users/models.py b/apps/users/models.py index d339ac8..89fe7c2 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -1,4 +1,5 @@ import random +import string from datetime import timedelta # import service_clients @@ -112,6 +113,9 @@ class User(AbstractUser): otp_expire = models.DateTimeField(_('otp expire'), blank=True, null=True) otp_try = models.IntegerField(_('otp try'), blank=True, null=True, default=0) + ott = models.CharField(_('one time token'), max_length=36, blank=True, null=True) + ott_expire = models.DateTimeField(_('otp expire'), blank=True, null=True) + last_checkout_request = models.DateTimeField(_('otp expire'), max_length=30, blank=True, null=True) balance = models.IntegerField(_('balance'), default=0) @@ -144,9 +148,22 @@ class User(AbstractUser): self.otp_expire = timezone.now() + timedelta(minutes=5) self.otp_try = 0 + def set_ott(self): + # TODO: get and set application + if settings.DEBUG or self.phone_number in DEVELOPMENT_PHONE_NUMBERS: + self.ott = 'abcdef12345' + + else: + self.ott = ''.join(random.choice(string.ascii_letters+string.digits+string.punctuation) for _ in range(32)) + + self.ott_expire = timezone.now() + timedelta(seconds=300) + def otp_is_valid(self): return bool(self.otp and timezone.now() <= self.otp_expire) + def ott_is_valid(self): + return bool(self.ott and timezone.now() <= self.ott_expire) + def check_otp(self, otp): if self.otp_try <= MAX_OTP_TRY: result = otp and self.otp == otp and self.otp_is_valid() @@ -163,6 +180,20 @@ class User(AbstractUser): self.save() return result + def check_ott(self, ott:str): + ott = ott.strip() + if ott and self.ott == ott and self.ott_is_valid(): + self.ott = None + self.date_joined = timezone.now() + result = True + + else: + self.ott = None + result = False + + self.save() + return result + def check_auth(self, field, value): result = False if field == 'otp': @@ -171,6 +202,9 @@ class User(AbstractUser): elif field == 'password': result = self.check_password(value) + elif field == 'ott': + result = self.check_ott(value) + if result: self.last_login = timezone.now() self.save() diff --git a/apps/users/serializers.py b/apps/users/serializers.py index c2a533b..fceb786 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -84,6 +84,18 @@ class RequestOTPSerializer(serializers.ModelSerializer): return result +class RequestOTTSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ( + 'ott', + 'ott_expire', + ) + + read_only_fields = ['ott', 'otp_expire'] + write_only_fields = [] + + class UserInquirySerializer(serializers.ModelSerializer): phone_number = serializers.CharField(required=True, validators=[phone_number_validator]) diff --git a/apps/users/urls.py b/apps/users/urls.py index 6ea61db..78b6ae9 100644 --- a/apps/users/urls.py +++ b/apps/users/urls.py @@ -1,7 +1,7 @@ from django.urls import path from django.contrib.auth.views import LogoutView from .views import UserListView, UserDetailView, AccountView, RequestOTPView, ChangePasswordView, \ - OTPLoginView, ProfileDetailView, ProfileUpdateView + OTPLoginView, ProfileDetailView, ProfileUpdateView, RequestOTTView app_name = "users" @@ -14,5 +14,6 @@ urlpatterns = [ path('api/users/', UserListView.as_view(), name='user_list_api'), path('api/users//', UserDetailView.as_view(), name='user_detail_api'), path('api/request_otp/', RequestOTPView.as_view(), name='request_otp_api'), + path('api/request_ott/', RequestOTTView.as_view(), name='request_ott_api'), path('api/change_password/', ChangePasswordView.as_view(), name='change_password_api'), ] diff --git a/apps/users/views.py b/apps/users/views.py index d4162ce..4f9aeac 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -15,7 +15,7 @@ from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRe from apps.users.forms import OTPAuthenticationForm, ProfileUpdateForm from apps.users.models import User from apps.users.provinces_and_cities import State -from apps.users.serializers import PublicUserSerializer, AccountSerializer, RequestOTPSerializer, \ +from apps.users.serializers import PublicUserSerializer, AccountSerializer, RequestOTPSerializer, RequestOTTSerializer,\ ChangePasswordSerializer, UserInquirySerializer UserModel = get_user_model() @@ -63,6 +63,22 @@ class RequestOTPView(generics.CreateAPIView): required_scopes = [] +class RequestOTTView(generics.RetrieveAPIView): + permission_classes = [IsAuthenticatedOrTokenHasScope] + required_scopes = ['accounts.account:request_ott'] + serializer_class = RequestOTTSerializer + + def get_object(self): + return self.request.user + + def get(self, request, *args, **kwargs): + # TODO: move it to query set + user = self.get_object() + user.set_ott() + user.save() + return super().get(request, *args, **kwargs) + + class UserInquiryView(generics.CreateAPIView): serializer_class = UserInquirySerializer permission_classes = [IsAuthenticatedOrTokenHasScope] diff --git a/client.py b/client.py index a66c962..43cd265 100644 --- a/client.py +++ b/client.py @@ -18,7 +18,7 @@ class ApiClient(): self.phone_number = phone_number - def login(self, password): + def otp_login(self, password): phone_number = self.phone_number data = { @@ -26,7 +26,7 @@ class ApiClient(): "username": phone_number, "password": password, # "scope": 'introspection', - "scope": 'introspection accounts.profile:list accounts.profile:retrieve accounts.account:retrieve accounts.account:update accounts.profile:inquiry accounts.account:change_password wallet.transaction:list wallet.invoice:create wallet.transaction:retrieve wallet.invoice:pay wallet.invoice:receipt wallet.deposit:submit wallet.deposit:verify wallet.withdraw:submit wallet.withdraw:verify ', + "scope": 'introspection accounts.account:request_ott accounts.profile:list accounts.profile:retrieve accounts.account:retrieve accounts.account:update accounts.profile:inquiry accounts.account:change_password wallet.transaction:list wallet.invoice:create wallet.transaction:retrieve wallet.invoice:pay wallet.invoice:receipt wallet.deposit:submit wallet.deposit:verify wallet.withdraw:submit wallet.withdraw:verify ', "auth_fields": 'phone_number:otp' } auth = (OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET) @@ -41,6 +41,28 @@ class ApiClient(): return auth_data + def ott_login(self, token): + phone_number = self.phone_number + + data = { + "grant_type": "password", + "username": phone_number, + "password": token, + "scope": 'introspection accounts.account:request_ott accounts.profile:list accounts.profile:retrieve accounts.account:retrieve accounts.account:update accounts.profile:inquiry accounts.account:change_password wallet.transaction:list wallet.invoice:create wallet.transaction:retrieve wallet.invoice:pay wallet.invoice:receipt wallet.deposit:submit wallet.deposit:verify wallet.withdraw:submit wallet.withdraw:verify ', + "auth_fields": 'phone_number:ott' + } + auth = (OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET) + + response = requests.post(f'{API_URI}/oauth2/token/', + data=data, + auth=auth) + + auth_data = response.json() + if 'access_token' in auth_data: + self.auth_data = auth_data + + return auth_data + def _request(self, path, data=None, files=None, method='get', with_auth=True, encode=True): if files: header = {} @@ -144,6 +166,10 @@ class ApiClient(): # return response and 'code' in response, response # create application invoice + def get_application_token(self): + response = self._request('users/api/request_ott/', method='get') + return response and 'code' in response, response + def create_invoice(self, amount, delay=0): data = { 'delay': delay, @@ -191,6 +217,9 @@ class ApiClient(): client = ApiClient('+989106853582') client.request_otp() -client.login('12345') -print(client.create_invoice(2000, 10)) +client.otp_login('12345') +print(client.get_application_token()) +print(client.ott_login('abcdef12345')) + +# print(client.create_invoice(2000, 10)) # print(client.pay_invoice('7967a76a-5de1-48b2-92ac-1b0f39f7223b')) diff --git a/templates/new_base.html b/templates/new_base.html deleted file mode 100755 index c551dbb..0000000 --- a/templates/new_base.html +++ /dev/null @@ -1,39 +0,0 @@ -{% load static %} - - - - - Login - - - - - - - - - - - - - -
-
-
- {#
#} - {# #} - {#
#} -
-
- {% block content %} - {% endblock %} -
-
- - -
-
-
- - -