cic-stack/apps/cic-ussd/cic_ussd/db/models/account.py

211 lines
7.8 KiB
Python
Raw Normal View History

2021-02-06 16:13:47 +01:00
# standard imports
2021-08-06 18:29:01 +02:00
import json
# external imports
from cic_eth.api import Api
from cic_types.condiments import MetadataPointer
2021-11-29 22:24:37 +01:00
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm.session import Session
2021-02-06 16:13:47 +01:00
# local imports
2021-08-06 18:29:01 +02:00
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
2021-06-29 12:49:25 +02:00
from cic_ussd.db.enum import AccountStatus
2021-02-06 16:13:47 +01:00
from cic_ussd.db.models.base import SessionBase
2021-08-06 18:29:01 +02:00
from cic_ussd.db.models.task_tracker import TaskTracker
2021-02-06 16:13:47 +01:00
from cic_ussd.encoder import check_password_hash, create_password_hash
2021-11-29 22:24:37 +01:00
from cic_ussd.phone_number import Support
support_phone = Support.phone_number
2021-02-06 16:13:47 +01:00
2021-04-19 10:44:40 +02:00
class Account(SessionBase):
2021-02-06 16:13:47 +01:00
"""
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.
"""
2021-04-19 10:44:40 +02:00
__tablename__ = 'account'
2021-02-06 16:13:47 +01:00
blockchain_address = Column(String)
phone_number = Column(String)
password_hash = Column(String)
failed_pin_attempts = Column(Integer)
2021-08-06 18:29:01 +02:00
status = Column(Integer)
2021-02-06 16:13:47 +01:00
preferred_language = Column(String)
2021-11-29 22:24:37 +01:00
guardians = Column(String)
guardian_quora = Column(Integer)
2021-02-06 16:13:47 +01:00
2021-08-06 18:29:01 +02:00
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
2021-11-29 22:24:37 +01:00
# self.guardians = f'{support_phone}' if support_phone else None
self.guardian_quora = 1
2021-08-06 18:29:01 +02:00
self.status = AccountStatus.PENDING.value
def __repr__(self):
return f'<Account: {self.blockchain_address}>'
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
2021-11-29 22:24:37 +01:00
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
2021-08-06 18:29:01 +02:00
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)
2021-06-29 12:49:25 +02:00
@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
2021-08-06 18:29:01 +02:00
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)
2021-02-06 16:13:47 +01:00
self.failed_pin_attempts = 0
2021-08-06 18:29:01 +02:00
self.status = AccountStatus.RESET.value
session.add(self)
session.flush()
SessionBase.release_session(session=session)
2021-10-07 17:12:35 +02:00
return 'Pin reset successful.'
2021-02-06 16:13:47 +01:00
2021-08-06 18:29:01 +02:00
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
"""
2021-10-07 17:12:35 +02:00
identifier = bytes.fromhex(self.blockchain_address)
key = cache_data_key(identifier, MetadataPointer.PERSON)
2021-08-06 18:29:01 +02:00
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)
2021-02-06 16:13:47 +01:00
2021-08-06 18:29:01 +02:00
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
2021-02-06 16:13:47 +01:00
"""
2021-08-06 18:29:01 +02:00
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
2021-02-06 16:13:47 +01:00
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)
2021-08-06 18:29:01 +02:00
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)
2021-02-06 16:13:47 +01:00
2021-08-06 18:29:01 +02:00
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)