BugFix in verification subsystem

This commit is contained in:
vlados31 2022-09-24 16:38:17 +03:00
parent 44da6faadd
commit 2c51925827
5 changed files with 108 additions and 26 deletions

View File

@ -1,5 +1,4 @@
from django.apps import AppConfig from django.apps import AppConfig
from .models import PhoneVerificationService
class AccountConfig(AppConfig): class AccountConfig(AppConfig):
@ -7,4 +6,6 @@ class AccountConfig(AppConfig):
name = 'account' name = 'account'
def ready(self): def ready(self):
from .models import PhoneVerificationService
PhoneVerificationService.create() PhoneVerificationService.create()
return True

View File

@ -37,14 +37,13 @@ class PhoneVerificationService:
with PhoneVerificationService.__lock: with PhoneVerificationService.__lock:
if PhoneVerificationService.__event.is_set(): if PhoneVerificationService.__event.is_set():
PhoneVerificationService.__event.clear() PhoneVerificationService.__event.clear()
print("Event reached!")
phones = PhoneVerificationService.__to_verify.copy() phones = PhoneVerificationService.__to_verify.copy()
PhoneVerificationService.__to_verify.clear() PhoneVerificationService.__to_verify.clear()
if phones is not None: if phones is not None:
for phone in phones: for phone in phones:
# тут должна быть проверка, есть ли телефон в списке кодов # тут должна быть проверка, есть ли телефон в списке кодов
print(f"Verify {phone}")
obj = { obj = {
"code": None, "code": None,
"attempts": PHONE_VERIFICATION_ATTEMPTS, "attempts": PHONE_VERIFICATION_ATTEMPTS,
@ -68,6 +67,7 @@ class PhoneVerificationService:
if res_json["status"] == "OK": if res_json["status"] == "OK":
with PhoneVerificationService.__lock: with PhoneVerificationService.__lock:
PhoneVerificationService.__codes[phone]["code"] = res_json["code"] PhoneVerificationService.__codes[phone]["code"] = res_json["code"]
print(f"Verify code for {phone}: {res_json['code']}")
else: else:
with PhoneVerificationService.__lock: with PhoneVerificationService.__lock:
PhoneVerificationService.__codes[phone]["code"] = "FAILED" PhoneVerificationService.__codes[phone]["code"] = "FAILED"
@ -79,41 +79,50 @@ class PhoneVerificationService:
else: else:
with PhoneVerificationService.__lock: with PhoneVerificationService.__lock:
obj["code"] = random.randint(1000, 9999) obj["code"] = random.randint(1000, 9999)
PhoneVerificationService.__codes[phone] = obj
print(f"Verify code for {phone}: {obj['code']}")
except: except:
traceback.print_exc() traceback.print_exc()
@staticmethod @staticmethod
def create(): def create():
if PhoneVerificationService.__instance is None: if PhoneVerificationService.__instance is None:
PhoneVerificationService.__instance = Thread(target=PhoneVerificationService.__service_run) PhoneVerificationService.__instance = Thread(target=PhoneVerificationService.__service_run, daemon=True)
PhoneVerificationService.__instance.start() PhoneVerificationService.__instance.start()
CHECK_PHONE_NOT_FOUND = "not-found"
CHECK_PHONE_NOT_READY = "not-ready"
CHECK_PHONE_FAILED = "failed"
CHECK_PHONE_INVALID_CODE = "invalid"
CHECK_PHONE_MAX_ATTEMPTS = "max-attempts"
@staticmethod @staticmethod
def check_code(phone: str, code: int): def check_code(phone: str, code: int):
with PhoneVerificationService.__lock: with PhoneVerificationService.__lock:
if phone not in PhoneVerificationService.__codes: if phone not in PhoneVerificationService.__codes:
return False return False, PhoneVerificationService.CHECK_PHONE_NOT_FOUND
c = PhoneVerificationService.__codes[phone] c = PhoneVerificationService.__codes[phone]
print(f"verify struct: {c}")
if c["code"] is None: if c["code"] is None:
print(f"[PhoneVerificationService] for phone {phone} code not received yet") print(f"[PhoneVerificationService] for phone {phone} code not received yet")
return False return False, PhoneVerificationService.CHECK_PHONE_NOT_READY
if c["code"] == "FAILED": if c["code"] == "FAILED":
PhoneVerificationService.__codes.pop(phone) PhoneVerificationService.__codes.pop(phone)
return False return False, PhoneVerificationService.CHECK_PHONE_FAILED
if c["attempts"] > 0: if c["attempts"] > 0:
if c["code"] == code: if c["code"] == code:
PhoneVerificationService.__codes.pop(phone) PhoneVerificationService.__codes.pop(phone)
return True return True, None
else:
c["attepts"] -= 1
return False
else:
raise Exception("Number of attempts exceeded")
return False else:
print(f"invalid code! attempts: {c['attempts']}")
PhoneVerificationService.__codes[phone]["attempts"] -= 1
return False, PhoneVerificationService.CHECK_PHONE_INVALID_CODE
else:
return False, PhoneVerificationService.CHECK_PHONE_MAX_ATTEMPTS
@staticmethod @staticmethod
def send_verify(phone: str): def send_verify(phone: str):
@ -129,6 +138,7 @@ class SiteAccountManager(BaseUserManager):
user.set_password(password) user.set_password(password)
user.is_staff = False user.is_staff = False
user.is_superuser = False user.is_superuser = False
user.is_phone_verified = False
user.save(using=self._db) user.save(using=self._db)
return user return user
@ -161,12 +171,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="^\\+7[0-9]*$"), # +79208109798 RegexValidator(regex="^\\+7[0-9]*$"),
MaxLengthValidator(limit_value=12), MaxLengthValidator(limit_value=12),
MinLengthValidator(limit_value=12) 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="Телефон верифицирован") is_phone_verified = models.BooleanField(default=False, verbose_name="Телефон верифицирован")
REQUIRED_FIELDS = ['name', 'surname', 'phone'] REQUIRED_FIELDS = ['name', 'surname', 'phone']
USERNAME_FIELD = 'email' USERNAME_FIELD = 'email'

View File

@ -1,4 +1,5 @@
import traceback import traceback
from account.models import PhoneVerificationService
# как создавать ошибку # как создавать ошибку
# raise Exception(API_ERROR_XXX, <related_obj>) # raise Exception(API_ERROR_XXX, <related_obj>)
@ -20,6 +21,22 @@ API_ERROR_INVALID_TOKEN = (503, 'invalid token')
# времненное решение, позже нужно будет заменить на конкретные ошибки # времненное решение, позже нужно будет заменить на конкретные ошибки
API_ERROR_USER_REGISTER = (510, 'user registration error') API_ERROR_USER_REGISTER = (510, 'user registration error')
API_ERROR_VALIDATION_INVALID_CODE = (520, 'invalid code')
API_ERROR_VALIDATION_MAX_ATTEMPTS = (521, 'max attempts')
API_ERROR_VALIDATION_CURRENTLY_VERIFIED = (522, 'currently phone is verified')
API_ERROR_VALIDATION_FAILED = (523, 'cannot be verified')
API_ERROR_VALIDATION_NOT_READY = (524, 'verification service not ready. call this method later')
API_ERROR_VALIDATION_NOT_FOUND = (525, 'verification service did not send code. call this method without \'code\'')
API_ERROR_VALIDATION_UNKNOWN = (526, 'unknown verification error')
API_ERROR_VALIDATION = {
PhoneVerificationService.CHECK_PHONE_INVALID_CODE: API_ERROR_VALIDATION_INVALID_CODE,
PhoneVerificationService.CHECK_PHONE_MAX_ATTEMPTS: API_ERROR_VALIDATION_MAX_ATTEMPTS,
PhoneVerificationService.CHECK_PHONE_FAILED: API_ERROR_VALIDATION_FAILED,
PhoneVerificationService.CHECK_PHONE_NOT_READY: API_ERROR_VALIDATION_NOT_READY,
PhoneVerificationService.CHECK_PHONE_NOT_FOUND: API_ERROR_VALIDATION_NOT_FOUND,
}
def make_error_object(ex: Exception): def make_error_object(ex: Exception):
try: try:

View File

@ -1,5 +1,5 @@
import traceback import traceback
import account.models from account.models import *
from .api_utils import * from .api_utils import *
from .models import * from .models import *
from django.core.exceptions import * from django.core.exceptions import *
@ -26,7 +26,7 @@ def account_register(params):
email = api_get_param_str(params, "email") email = api_get_param_str(params, "email")
password = api_get_param_str(params, "password") password = api_get_param_str(params, "password")
user = account.models.SiteUser( user = SiteUser(
name=name, name=name,
surname=surname, surname=surname,
phone=phone, phone=phone,
@ -47,14 +47,55 @@ def account_register(params):
user.delete() user.delete()
raise ex raise ex
except ValidationError as e: except ValidationError as validation_error:
traceback.print_exc() traceback.print_exc()
raise Exception(API_ERROR_USER_REGISTER, e.message_dict) errors = {}
for field_name in validation_error.error_dict:
err_list = validation_error.error_dict[field_name]
print(err_list)
obj = []
for err in err_list:
obj.append({
"code": err.code
})
errors[field_name] = obj
raise Exception(API_ERROR_USER_REGISTER, errors)
def account_verify_phone(params):
user = _reqire_access_token(params)
if user.is_phone_verified:
raise Exception(API_ERROR_VALIDATION_CURRENTLY_VERIFIED)
code = api_get_param_int(params, "code", False, None)
if code is None:
PhoneVerificationService.send_verify(user.phone)
return api_make_response({"action": "phone_call"})
else:
res, err_code = PhoneVerificationService.check_code(user.phone, code)
if res:
user.is_phone_verified = True
user.save()
return api_make_response({"status": "success"})
else:
if err_code in API_ERROR_VALIDATION:
raise Exception(API_ERROR_VALIDATION[err_code])
else:
raise Exception(API_ERROR_VALIDATION_UNKNOWN)
def account_get(params): def account_get(params):
user = _reqire_access_token(params) user = _reqire_access_token(params)
return api_make_response({"name": user.name, "surname": user.surname, "email": user.email, "phone": user.phone}) return api_make_response({
"name": user.name,
"surname": user.surname,
"email": user.email,
"phone": user.phone,
"phone_verified": user.is_phone_verified
})
def __make_argument_doc(name, arg_type, description, required=True): def __make_argument_doc(name, arg_type, description, required=True):
@ -83,13 +124,25 @@ api_methods = {
], ],
"returns": "В случае правильных логина и пароля <code>access_token</code>. В противном случае объект ошибки." "returns": "В случае правильных логина и пароля <code>access_token</code>. В противном случае объект ошибки."
}, },
"account.register": { "account.register": {
"func": account_register, "func": account_register,
"doc": "Регистрация нового пользователя", "doc": "Регистрация нового пользователя",
"params": [ "params": [
], ],
"returns": "Поля пользователя (name, surname, email, phone)." "returns": "Поля пользователя (name, surname, email, phone, phone_verified)."
},
"account.verifyPhone": {
"func": account_verify_phone,
"doc": "Запросить верификацию номера телефона."
"Если телефон уже верифицирован, метод вернет соответствующую ошибку",
"params": [
__make_argument_access_token(),
__make_argument_doc("code", __doc_type_string, "Код верификации. Если не передать, будет выполнен звонок"),
],
"returns": '{"status": "success"}, если верификация пройдена. Иначе одну из стандартных ошибок'
}, },
"account.get": { "account.get": {
@ -98,6 +151,6 @@ api_methods = {
"params": [ "params": [
__make_argument_access_token() __make_argument_access_token()
], ],
"returns": "Поля пользователя (name, surname, email, phone)." "returns": "Поля пользователя (name, surname, email, phone, phone_verified)."
}, },
} }

View File

@ -45,10 +45,11 @@ class UserToken(models.Model):
@staticmethod @staticmethod
def get_user_by_token(token: str): def get_user_by_token(token: str):
t = UserToken.objects.get(access_token=token) t = UserToken.objects.filter(access_token=token)
if t is None:
if len(t) == 0:
raise Exception(API_ERROR_INVALID_TOKEN) raise Exception(API_ERROR_INVALID_TOKEN)
return t.user return t[0].user
def __str__(self): def __str__(self):
return self.user.email + ": " + self.access_token[:10] + "..." return self.user.email + ": " + self.access_token[:10] + "..."