diff --git a/account/apps.py b/account/apps.py index 2b08f1a..9eadc66 100644 --- a/account/apps.py +++ b/account/apps.py @@ -1,6 +1,10 @@ from django.apps import AppConfig +from .models import PhoneVerificationService class AccountConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'account' + + def ready(self): + PhoneVerificationService.create() diff --git a/account/models.py b/account/models.py index 6a53318..3998c21 100644 --- a/account/models.py +++ b/account/models.py @@ -1,6 +1,126 @@ from django.db import models from django.contrib.auth.models import PermissionsMixin, AbstractBaseUser, BaseUserManager from django.core.validators import * +from arka.settings import PHONE_VERIFICATION_ENABLE, PHONE_VERIFICATION_ATTEMPTS, PHONE_VERIFICATION_APP_ID + +from threading import Thread, Lock, Event +import random +import traceback +from datetime import datetime +import requests + + +class PhoneVerificationService: + __lock = Lock() + __event = Event() + + # номера, для которых отправлен запрос верификации (на внешний ресурс) + # имеет структуру: + # "{phone}": {"code": None|int|"FAILED", "attempts": int} + # None в code означает что ответ еще не пришел, остальное вроде понятно + # attempts - сколько попыток верификации осталось (по умолчанию 5) + __codes = {} + + # очередь номеров, которые требуют верификации + __to_verify = [] + __instance = None + + @staticmethod + def __service_run(): + while True: + try: + + # TODO запилить логику, которая предотвратит злоупотребление вызовами со стороны пользователей + + PhoneVerificationService.__event.wait() + phones = None + with PhoneVerificationService.__lock: + if PhoneVerificationService.__event.is_set(): + PhoneVerificationService.__event.clear() + print("Event reached!") + phones = PhoneVerificationService.__to_verify.copy() + PhoneVerificationService.__to_verify.clear() + + if phones is not None: + for phone in phones: + # тут должна быть проверка, есть ли телефон в списке кодов + print(f"Verify {phone}") + obj = { + "code": None, + "attempts": PHONE_VERIFICATION_ATTEMPTS, + "time": datetime.now() + } + if PHONE_VERIFICATION_ENABLE: + with PhoneVerificationService.__lock: + PhoneVerificationService.__codes[phone] = obj + + request_success = False + try: + params = { + "phone": phone, + "ip": -1, + "api_id": PHONE_VERIFICATION_APP_ID + } + res = requests.get("https://sms.ru/code/call", params=params, timeout=5) + res_json = res.json() + request_success = True + print(res.content) + if res_json["status"] == "OK": + with PhoneVerificationService.__lock: + PhoneVerificationService.__codes[phone]["code"] = res_json["code"] + else: + with PhoneVerificationService.__lock: + PhoneVerificationService.__codes[phone]["code"] = "FAILED" + except: + if not request_success: + with PhoneVerificationService.__lock: + PhoneVerificationService.__codes[phone]["code"] = "FAILED" + traceback.print_exc() + else: + with PhoneVerificationService.__lock: + obj["code"] = random.randint(1000, 9999) + except: + traceback.print_exc() + + @staticmethod + def create(): + if PhoneVerificationService.__instance is None: + PhoneVerificationService.__instance = Thread(target=PhoneVerificationService.__service_run) + PhoneVerificationService.__instance.start() + + @staticmethod + def check_code(phone: str, code: int): + with PhoneVerificationService.__lock: + if phone not in PhoneVerificationService.__codes: + return False + c = PhoneVerificationService.__codes[phone] + + if c["code"] is None: + print(f"[PhoneVerificationService] for phone {phone} code not received yet") + return False + + if c["code"] == "FAILED": + PhoneVerificationService.__codes.pop(phone) + return False + + if c["attempts"] > 0: + if c["code"] == code: + PhoneVerificationService.__codes.pop(phone) + return True + else: + c["attepts"] -= 1 + return False + else: + raise Exception("Number of attempts exceeded") + + return False + + @staticmethod + def send_verify(phone: str): + with PhoneVerificationService.__lock: + PhoneVerificationService.__to_verify.append(phone) + + PhoneVerificationService.__event.set() class SiteAccountManager(BaseUserManager): @@ -41,11 +161,12 @@ class SiteUser(AbstractBaseUser, PermissionsMixin): name = models.CharField(max_length=60, verbose_name="Имя") email = models.EmailField(unique=True, verbose_name="Email") phone = models.CharField(unique=True, max_length=16, verbose_name="Телефон", validators=[ - RegexValidator(regex="^\\+?[0-9]*$"), - MaxLengthValidator(limit_value=16), - MinLengthValidator(limit_value=6) + RegexValidator(regex="^\\+7[0-9]*$"), # +79208109798 + MaxLengthValidator(limit_value=12), + MinLengthValidator(limit_value=12) ]) is_staff = models.BooleanField(default=False, verbose_name="Разрешение на вход в админку") + is_valid_phone = models.BooleanField(default=False, verbose_name="Телефон верифицирован") REQUIRED_FIELDS = ['name', 'surname', 'phone'] USERNAME_FIELD = 'email' diff --git a/account/urls.py b/account/urls.py index e30a8c1..683f148 100644 --- a/account/urls.py +++ b/account/urls.py @@ -18,8 +18,8 @@ from django.urls import path from . import views urlpatterns = [ - path('', views.index, name='account'), path('register', views.register, name='register'), + path('profile/', views.profile, name='profile'), # path('account', views.account, name='account'), # path('account_', views.account_action, name='account_action'), # diff --git a/account/views.py b/account/views.py index 75f575d..a5e4255 100644 --- a/account/views.py +++ b/account/views.py @@ -5,7 +5,7 @@ from django.contrib.auth import login, authenticate def index(request): - return render(request, 'account.html') + return render(request, 'profile.html') def register(request): @@ -22,3 +22,7 @@ def register(request): else: form = SiteUserForm() return render(request, 'registration/register.html', {'form': form}) + + +def profile(request): + return render(request, 'profile.html') diff --git a/arka/settings.py b/arka/settings.py index 1895903..c08528d 100644 --- a/arka/settings.py +++ b/arka/settings.py @@ -143,4 +143,7 @@ STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -LOGIN_REDIRECT_URL = '/account/' +# LOGIN_REDIRECT_URL = '/account/' +PHONE_VERIFICATION_ENABLE = False +PHONE_VERIFICATION_ATTEMPTS = 5 +PHONE_VERIFICATION_APP_ID = "ACC90F4A-FE4A-5137-45C9-5D9E84DE9440" diff --git a/arka/urls.py b/arka/urls.py index 38eeb4a..3ef3248 100644 --- a/arka/urls.py +++ b/arka/urls.py @@ -19,8 +19,8 @@ from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), - path('account/', include('account.urls')), path('accounts/', include('django.contrib.auth.urls')), + path('accounts/', include('account.urls')), path('api/', include('api.urls')), path('dev/', include('dev.urls')), path('orders/', include('order.urls')),