# standard imports import json # external imports from cic_eth.api import Api from cic_types.condiments import MetadataPointer from sqlalchemy import Column, Integer, String from sqlalchemy.orm.session import Session # local imports from cic_ussd.account.metadata import get_cached_preferred_language, parse_account_metadata from cic_ussd.cache import Cache, cache_data_key, get_cached_data from cic_ussd.db.enum import AccountStatus from cic_ussd.db.models.base import SessionBase from cic_ussd.db.models.task_tracker import TaskTracker from cic_ussd.encoder import check_password_hash, create_password_hash from cic_ussd.phone_number import Support support_phone = Support.phone_number class Account(SessionBase): """ This class defines a user record along with functions responsible for hashing the user's corresponding password and subsequently verifying a password's validity given an input to compare against the persisted hash. """ __tablename__ = 'account' blockchain_address = Column(String) phone_number = Column(String) password_hash = Column(String) failed_pin_attempts = Column(Integer) status = Column(Integer) preferred_language = Column(String) guardians = Column(String) guardian_quora = Column(Integer) def __init__(self, blockchain_address, phone_number): self.blockchain_address = blockchain_address self.phone_number = phone_number self.password_hash = None self.failed_pin_attempts = 0 # self.guardians = f'{support_phone}' if support_phone else None self.guardian_quora = 1 self.status = AccountStatus.PENDING.value def __repr__(self): return f'' def activate_account(self): """This method is used to reset failed pin attempts and change account status to Active.""" self.failed_pin_attempts = 0 self.status = AccountStatus.ACTIVE.value def add_guardian(self, phone_number: str): set_guardians = phone_number if self.guardians: set_guardians = self.guardians.split(',') set_guardians.append(phone_number) ','.join(set_guardians) self.guardians = set_guardians def remove_guardian(self, phone_number: str): set_guardians = self.guardians.split(',') set_guardians.remove(phone_number) if len(set_guardians) > 1: self.guardians = ','.join(set_guardians) else: self.guardians = set_guardians[0] def get_guardians(self) -> list: return self.guardians.split(',') if self.guardians else [] def set_guardian_quora(self, quora: int): self.guardian_quora = quora def create_password(self, password): """This method takes a password value and hashes the value before assigning it to the corresponding `hashed_password` attribute in the user record. :param password: A password value :type password: str """ self.password_hash = create_password_hash(password) @staticmethod def get_by_phone_number(phone_number: str, session: Session): """Retrieves an account from a phone number. :param phone_number: The E164 format of a phone number. :type phone_number:str :param session: :type session: :return: An account object. :rtype: Account """ session = SessionBase.bind_session(session=session) account = session.query(Account).filter_by(phone_number=phone_number).first() SessionBase.release_session(session=session) return account def has_preferred_language(self) -> bool: return get_cached_preferred_language(self.blockchain_address) is not None def has_valid_pin(self, session: Session): """ :param session: :type session: :return: :rtype: """ return self.get_status(session) == AccountStatus.ACTIVE.name and self.password_hash is not None def pin_is_blocked(self, session: Session) -> bool: """ :param session: :type session: :return: :rtype: """ return self.failed_pin_attempts == 3 and self.get_status(session) == AccountStatus.LOCKED.name def reset_pin(self, session: Session) -> str: """This function resets the number of failed pin attempts to zero. It places the account in pin reset status enabling users to reset their pins. :param session: Database session object. :type session: Session """ session = SessionBase.bind_session(session=session) self.failed_pin_attempts = 0 self.status = AccountStatus.RESET.value session.add(self) session.flush() SessionBase.release_session(session=session) return 'Pin reset successful.' def standard_metadata_id(self) -> str: """This function creates an account's standard metadata identification information that contains an account owner's given name, family name and phone number and defaults to a phone number in the absence of metadata. :return: Standard metadata identification information | e164 formatted phone number. :rtype: str """ identifier = bytes.fromhex(self.blockchain_address) key = cache_data_key(identifier, MetadataPointer.PERSON) account_metadata = get_cached_data(key) if not account_metadata: return self.phone_number account_metadata = json.loads(account_metadata) return parse_account_metadata(account_metadata) def get_status(self, session: Session): """This function handles account status queries, it checks whether an account's failed pin attempts exceed 2 and updates the account status locked, it then returns the account status :return: The account status for a user object :rtype: str """ session = SessionBase.bind_session(session=session) if self.failed_pin_attempts > 2: self.status = AccountStatus.LOCKED.value session.add(self) session.flush() SessionBase.release_session(session=session) return AccountStatus(self.status).name def verify_password(self, password): """This method takes a password value and compares it to the user's corresponding `hashed_password` value to establish password validity. :param password: A password value :type password: str :return: Pin validity :rtype: boolean """ return check_password_hash(password, self.password_hash) def create(chain_str: str, phone_number: str, session: Session): """ :param chain_str: :type chain_str: :param phone_number: :type phone_number: :param session: :type session: :return: :rtype: """ api = Api(callback_task='cic_ussd.tasks.callback_handler.account_creation_callback', callback_queue='cic-ussd', callback_param='', chain_str=chain_str) task_uuid = api.create_account().id TaskTracker.add(session=session, task_uuid=task_uuid) cache_creation_task_uuid(phone_number=phone_number, task_uuid=task_uuid) def cache_creation_task_uuid(phone_number: str, task_uuid: str): """This function stores the task id that is returned from a task spawned to create a blockchain account in the redis cache. :param phone_number: The phone number for the user whose account is being created. :type phone_number: str :param task_uuid: A celery task id :type task_uuid: str """ cache = Cache.store account_creation_request_data = { 'phone_number': phone_number, 'sms_notification_sent': False, 'status': 'PENDING', 'task_uuid': task_uuid } cache.set(task_uuid, json.dumps(account_creation_request_data)) cache.persist(name=task_uuid)