Add phone verification logic
This commit is contained in:
parent
e10b2e0138
commit
44da6faadd
@ -1,6 +1,10 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from .models import PhoneVerificationService
|
||||||
|
|
||||||
|
|
||||||
class AccountConfig(AppConfig):
|
class AccountConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'account'
|
name = 'account'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
PhoneVerificationService.create()
|
||||||
|
@ -1,6 +1,126 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import PermissionsMixin, AbstractBaseUser, BaseUserManager
|
from django.contrib.auth.models import PermissionsMixin, AbstractBaseUser, BaseUserManager
|
||||||
from django.core.validators import *
|
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):
|
class SiteAccountManager(BaseUserManager):
|
||||||
@ -41,11 +161,12 @@ class SiteUser(AbstractBaseUser, PermissionsMixin):
|
|||||||
name = models.CharField(max_length=60, verbose_name="Имя")
|
name = models.CharField(max_length=60, verbose_name="Имя")
|
||||||
email = models.EmailField(unique=True, verbose_name="Email")
|
email = models.EmailField(unique=True, verbose_name="Email")
|
||||||
phone = models.CharField(unique=True, max_length=16, verbose_name="Телефон", validators=[
|
phone = models.CharField(unique=True, max_length=16, verbose_name="Телефон", validators=[
|
||||||
RegexValidator(regex="^\\+?[0-9]*$"),
|
RegexValidator(regex="^\\+7[0-9]*$"), # +79208109798
|
||||||
MaxLengthValidator(limit_value=16),
|
MaxLengthValidator(limit_value=12),
|
||||||
MinLengthValidator(limit_value=6)
|
MinLengthValidator(limit_value=12)
|
||||||
])
|
])
|
||||||
is_staff = models.BooleanField(default=False, verbose_name="Разрешение на вход в админку")
|
is_staff = models.BooleanField(default=False, verbose_name="Разрешение на вход в админку")
|
||||||
|
is_valid_phone = models.BooleanField(default=False, verbose_name="Телефон верифицирован")
|
||||||
REQUIRED_FIELDS = ['name', 'surname', 'phone']
|
REQUIRED_FIELDS = ['name', 'surname', 'phone']
|
||||||
USERNAME_FIELD = 'email'
|
USERNAME_FIELD = 'email'
|
||||||
|
|
||||||
|
@ -18,8 +18,8 @@ from django.urls import path
|
|||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.index, name='account'),
|
|
||||||
path('register', views.register, name='register'),
|
path('register', views.register, name='register'),
|
||||||
|
path('profile/', views.profile, name='profile'),
|
||||||
# path('account', views.account, name='account'),
|
# path('account', views.account, name='account'),
|
||||||
# path('account_<str:action>', views.account_action, name='account_action'),
|
# path('account_<str:action>', views.account_action, name='account_action'),
|
||||||
#
|
#
|
||||||
|
@ -5,7 +5,7 @@ from django.contrib.auth import login, authenticate
|
|||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
return render(request, 'account.html')
|
return render(request, 'profile.html')
|
||||||
|
|
||||||
|
|
||||||
def register(request):
|
def register(request):
|
||||||
@ -22,3 +22,7 @@ def register(request):
|
|||||||
else:
|
else:
|
||||||
form = SiteUserForm()
|
form = SiteUserForm()
|
||||||
return render(request, 'registration/register.html', {'form': form})
|
return render(request, 'registration/register.html', {'form': form})
|
||||||
|
|
||||||
|
|
||||||
|
def profile(request):
|
||||||
|
return render(request, 'profile.html')
|
||||||
|
@ -143,4 +143,7 @@ STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")]
|
|||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
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"
|
||||||
|
@ -19,8 +19,8 @@ from django.urls import path, include
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('account/', include('account.urls')),
|
|
||||||
path('accounts/', include('django.contrib.auth.urls')),
|
path('accounts/', include('django.contrib.auth.urls')),
|
||||||
|
path('accounts/', include('account.urls')),
|
||||||
path('api/', include('api.urls')),
|
path('api/', include('api.urls')),
|
||||||
path('dev/', include('dev.urls')),
|
path('dev/', include('dev.urls')),
|
||||||
path('orders/', include('order.urls')),
|
path('orders/', include('order.urls')),
|
||||||
|
Reference in New Issue
Block a user