diff --git a/account/apps.py b/account/apps.py index 9eadc66..8b30ec1 100644 --- a/account/apps.py +++ b/account/apps.py @@ -1,5 +1,4 @@ from django.apps import AppConfig -from .models import PhoneVerificationService class AccountConfig(AppConfig): @@ -7,4 +6,6 @@ class AccountConfig(AppConfig): name = 'account' def ready(self): + from .models import PhoneVerificationService PhoneVerificationService.create() + return True diff --git a/account/models.py b/account/models.py index 3998c21..ed95216 100644 --- a/account/models.py +++ b/account/models.py @@ -37,14 +37,13 @@ class PhoneVerificationService: 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, @@ -68,6 +67,7 @@ class PhoneVerificationService: if res_json["status"] == "OK": with PhoneVerificationService.__lock: PhoneVerificationService.__codes[phone]["code"] = res_json["code"] + print(f"Verify code for {phone}: {res_json['code']}") else: with PhoneVerificationService.__lock: PhoneVerificationService.__codes[phone]["code"] = "FAILED" @@ -79,41 +79,50 @@ class PhoneVerificationService: else: with PhoneVerificationService.__lock: obj["code"] = random.randint(1000, 9999) + PhoneVerificationService.__codes[phone] = obj + print(f"Verify code for {phone}: {obj['code']}") except: traceback.print_exc() @staticmethod def create(): if PhoneVerificationService.__instance is None: - PhoneVerificationService.__instance = Thread(target=PhoneVerificationService.__service_run) + PhoneVerificationService.__instance = Thread(target=PhoneVerificationService.__service_run, daemon=True) 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 def check_code(phone: str, code: int): with PhoneVerificationService.__lock: if phone not in PhoneVerificationService.__codes: - return False + return False, PhoneVerificationService.CHECK_PHONE_NOT_FOUND c = PhoneVerificationService.__codes[phone] + print(f"verify struct: {c}") if c["code"] is None: print(f"[PhoneVerificationService] for phone {phone} code not received yet") - return False + return False, PhoneVerificationService.CHECK_PHONE_NOT_READY if c["code"] == "FAILED": PhoneVerificationService.__codes.pop(phone) - return False + return False, PhoneVerificationService.CHECK_PHONE_FAILED 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 True, None - 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 def send_verify(phone: str): @@ -129,6 +138,7 @@ class SiteAccountManager(BaseUserManager): user.set_password(password) user.is_staff = False user.is_superuser = False + user.is_phone_verified = False user.save(using=self._db) return user @@ -161,12 +171,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="^\\+7[0-9]*$"), # +79208109798 + RegexValidator(regex="^\\+7[0-9]*$"), 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="Телефон верифицирован") + is_phone_verified = models.BooleanField(default=False, verbose_name="Телефон верифицирован") REQUIRED_FIELDS = ['name', 'surname', 'phone'] USERNAME_FIELD = 'email' diff --git a/api/api_errors.py b/api/api_errors.py index a1c7f32..008542b 100644 --- a/api/api_errors.py +++ b/api/api_errors.py @@ -1,4 +1,5 @@ import traceback +from account.models import PhoneVerificationService # как создавать ошибку # raise Exception(API_ERROR_XXX, ) @@ -20,6 +21,22 @@ API_ERROR_INVALID_TOKEN = (503, 'invalid token') # времненное решение, позже нужно будет заменить на конкретные ошибки 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): try: diff --git a/api/api_methods.py b/api/api_methods.py index 9e0738e..9ae2a4f 100644 --- a/api/api_methods.py +++ b/api/api_methods.py @@ -1,5 +1,5 @@ import traceback -import account.models +from account.models import * from .api_utils import * from .models import * from django.core.exceptions import * @@ -26,7 +26,7 @@ def account_register(params): email = api_get_param_str(params, "email") password = api_get_param_str(params, "password") - user = account.models.SiteUser( + user = SiteUser( name=name, surname=surname, phone=phone, @@ -47,14 +47,55 @@ def account_register(params): user.delete() raise ex - except ValidationError as e: + except ValidationError as validation_error: 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): 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): @@ -83,13 +124,25 @@ api_methods = { ], "returns": "В случае правильных логина и пароля access_token. В противном случае объект ошибки." }, + "account.register": { "func": account_register, "doc": "Регистрация нового пользователя", "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": { @@ -98,6 +151,6 @@ api_methods = { "params": [ __make_argument_access_token() ], - "returns": "Поля пользователя (name, surname, email, phone)." + "returns": "Поля пользователя (name, surname, email, phone, phone_verified)." }, } diff --git a/api/models.py b/api/models.py index ca88adb..678b173 100644 --- a/api/models.py +++ b/api/models.py @@ -45,10 +45,11 @@ class UserToken(models.Model): @staticmethod def get_user_by_token(token: str): - t = UserToken.objects.get(access_token=token) - if t is None: + t = UserToken.objects.filter(access_token=token) + + if len(t) == 0: raise Exception(API_ERROR_INVALID_TOKEN) - return t.user + return t[0].user def __str__(self): return self.user.email + ": " + self.access_token[:10] + "..."