class Building(ModelBase): """ Establishes a building in the database. lat, lng, place_id, state etc. are all pulled from Google maps """ __tablename__ = 'building' lat = db.Column(db.Float()) lng = db.Column(db.Float()) formatted_address = db.Column(db.String()) place_id = db.Column(db.String()) state = db.Column(db.String()) country = db.Column(db.String()) locality = db.Column(db.String()) postal_code = db.Column(db.String()) route = db.Column(db.String()) street_number = db.Column(db.String()) url = db.Column(db.String()) roof_area_estimation = db.relationship( 'RoofAreaEstimation', backref='building', lazy='dynamic', foreign_keys='RoofAreaEstimation.building_id')
class Point(db.Model): __tablename__ = "points" id = db.Column(db.Integer, autoincrement=True, primary_key=True) date = db.Column(DateTime, default=datetime.datetime.utcnow) smell = db.Column(db.Boolean(), nullable=False) taste = db.Column(db.Boolean(), nullable=False) latitude = db.Column(db.Float(), nullable=False) longitude = db.Column(db.Float(), nullable=False)
class EstimationPoints(ModelBase): """ We store the paths of a polygon submitted by the user here These make up the roof area estimation area """ __tablename__ = 'estimation_points' latitude = db.Column(db.Float()) longitude = db.Column(db.Float()) roof_area_estimation_id = db.Column( db.Integer, db.ForeignKey('roof_area_estimation.id'))
class RoofAreaEstimation(ModelBase): """ Roof area estimation tied to a specific building. Estimated lat & lng stored to 5 decimal points """ __tablename__ = 'roof_area_estimation' area = db.Column(db.Float()) center_latitude = db.Column(db.Float()) center_longitude = db.Column(db.Float()) building_id = db.Column(db.Integer, db.ForeignKey("building.id")) estimation_points = db.relationship( 'EstimationPoints', backref='roof_area_estimation', lazy='dynamic', foreign_keys='EstimationPoints.roof_area_estimation_id')
class Run(db.Model): """Contains a set of screenshot records uploaded by a diff worker.""" DATA_PENDING = 'data_pending' DIFF_APPROVED = 'diff_approved' DIFF_FOUND = 'diff_found' DIFF_NOT_FOUND = 'diff_not_found' FAILED = 'failed' NEEDS_DIFF = 'needs_diff' NO_DIFF_NEEDED = 'no_diff_needed' STATES = frozenset([ DATA_PENDING, DIFF_APPROVED, DIFF_FOUND, DIFF_NOT_FOUND, FAILED, NEEDS_DIFF, NO_DIFF_NEEDED]) DIFF_NEEDED_STATES = frozenset([DIFF_FOUND, DIFF_APPROVED]) id = db.Column(db.Integer, primary_key=True) release_id = db.Column(db.Integer, db.ForeignKey('release.id')) release = db.relationship('Release', backref=db.backref('runs', lazy='select'), lazy='joined', join_depth=1) name = db.Column(db.String(255), nullable=False) # TODO: Put rigid DB constraint on uniqueness of (release_id, name) created = db.Column(db.DateTime, default=datetime.datetime.utcnow) modified = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) status = db.Column(db.Enum(*STATES), nullable=False) image = db.Column(db.String(100), db.ForeignKey('artifact.id')) log = db.Column(db.String(100), db.ForeignKey('artifact.id')) config = db.Column(db.String(100), db.ForeignKey('artifact.id')) url = db.Column(db.String(2048)) ref_image = db.Column(db.String(100), db.ForeignKey('artifact.id')) ref_log = db.Column(db.String(100), db.ForeignKey('artifact.id')) ref_config = db.Column(db.String(100), db.ForeignKey('artifact.id')) ref_url = db.Column(db.String(2048)) diff_image = db.Column(db.String(100), db.ForeignKey('artifact.id')) diff_log = db.Column(db.String(100), db.ForeignKey('artifact.id')) distortion = db.Column(db.Float()) tasks = db.relationship('WorkQueue', backref=db.backref('runs', lazy='select'), lazy='joined', join_depth=1, order_by='WorkQueue.created') # For flask-cache memoize key. def __repr__(self): return 'Run(id=%r)' % self.id
class Game(db.Model): """ This model is used to represent Company entries in our database. Attributes: Game_ID - ID of the game object. The ID will self-increment as we add more values into the Game table. Name - Name of the game. The IGDB API also pulls information like alternate names for the game, which may be integrated in a future project release. Image_URL - The image for the game. Rating - The rating the game received. This rating is pulled from IGDB, just like the other information regarding games. Release_Year - The year the game had came out. Associated_Companies - The companies who developed and published the game. This information will be populated using a combination of the association table and the IGDB API. Associated_Genres - The genres associated with the game. Like companies, the information is populated using an associated table with the data retrieved from the IGDB API. Associated_Platforms - The platforms the game was released on. This information is populated using a combination of the association table for platforms and games and the IGDB API. """ query_class = GameQuery __tablename__ = 'games' game_id = db.Column(db.Integer, primary_key=True, autoincrement=False) name = db.Column(db.String(50)) image_url = db.Column(db.String(255)) rating = db.Column(db.Float(4)) release_year = db.Column(db.Integer, db.ForeignKey('years.year_id')) search_vector = db.Column(TSVectorType('name')) associated_companies = db.relationship( "Company", secondary=association_table_game_company, backref=db.backref("games")) associated_genres = db.relationship("Genre", secondary=association_table_game_genre, backref=db.backref("games")) associated_platforms = db.relationship( "Platform", secondary=association_table_game_platform, backref=db.backref("games")) def __init__(self, id, name=None, image_url=None, rating=0.0, release_year=0, search_vector=None): self.game_id = id self.name = name self.image_url = image_url self.rating = rating self.release_year = release_year self.search_vector = search_vector def __repr__(self): return '<Game: %r>' % (self.name)
class Report(db.Model): __tablename__ = 'reports' id = db.Column(db.Integer, primary_key=True) results = db.Column(db.Text()) seo = db.Column(db.Float()) accessibility = db.Column(db.Float()) usability = db.Column(db.Float()) scanid = db.Column(db.Integer, db.ForeignKey('scans.id'), nullable=False) def __init__(self, scanid, seo, accessibility, usability, results): self.scanid = scanid self.seo = seo self.accessibility = accessibility self.usability = usability self.results = results db.session.add(self) db.session.commit() return def get_json_results(self): if self.results: return json.loads(self.results.replace("'",'"')) return None
class Invoice(db.Model): __tablename__ = 'invoices' id = db.Column(db.Integer, primary_key=True) datetime = db.Column(db.DateTime(), default=datetime.datetime.now()) ispaid = db.Column(db.Boolean(), default=False) paymentconfirmationid = db.Column(db.String(50)) discount = db.Column(db.Float(), default=0) amountdue = db.Column(db.Float(), nullable=False) tax = db.Column(db.Float(), nullable=False) description = db.Column(db.Text(), nullable=False) userid = db.Column(db.Integer(), db.ForeignKey('users.id'), nullable=False) def __repr__(self): return f'[Invoice] #{self.id}: On: {self.datetime} Due: {self.amountdue} Paid: {self.ispaid}' def __init__(self, amountdue, tax, description, userid): self.amountdue = amountdue self.tax = tax self.description = description self.userid = userid db.session.add(self) db.session.commit() return
class Recipe(db.Model): __tablename__ = 'recipe' id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, unique=True, nullable=False) name = db.Column(db.String(40), nullable=False) preparation = db.Column(db.String()) rating = db.Column(db.Float(), default=0) num_of_ratings = db.Column(db.Integer(), default=0) user_id = db.Column(UUID(as_uuid=True), db.ForeignKey('user.id'), nullable=False) num_of_ingredients = db.Column(db.Integer()) ingredients = db.relationship('Ingredient', secondary=recipe_ing, back_populates='recipes', lazy='dynamic') def __repr__(self): return f'<Recipe {self.name}>'
class User(ManyOrgBase, ModelBase, SoftDelete): """Establishes the identity of a user for both making transactions and more general interactions. Admin users are created through the auth api by registering an account with an email that has been pre-approved on the whitelist. By default, admin users only have minimal access levels (view). Permissions must be elevated manually in the database. Transaction-capable users (vendors and beneficiaries) are created using the POST user API or the bulk upload function """ __tablename__ = 'user' # override ModelBase deleted to add an index created = db.Column(db.DateTime, default=datetime.datetime.utcnow, index=True) first_name = db.Column(db.String()) last_name = db.Column(db.String()) preferred_language = db.Column(db.String()) primary_blockchain_address = db.Column(db.String()) _last_seen = db.Column(db.DateTime) email = db.Column(db.String()) _phone = db.Column(db.String(), unique=True, index=True) _public_serial_number = db.Column(db.String()) uuid = db.Column(db.String(), index=True) nfc_serial_number = db.Column(db.String()) password_hash = db.Column(db.String(200)) one_time_code = db.Column(db.String) secret = db.Column(db.String()) _TFA_secret = db.Column(db.String(128)) TFA_enabled = db.Column(db.Boolean, default=False) pin_hash = db.Column(db.String()) seen_latest_terms = db.Column(db.Boolean, default=False) failed_pin_attempts = db.Column(db.Integer, default=0) default_currency = db.Column(db.String()) _location = db.Column(db.String(), index=True) lat = db.Column(db.Float(), index=True) lng = db.Column(db.Float(), index=True) _held_roles = db.Column(JSONB) is_activated = db.Column(db.Boolean, default=False) is_disabled = db.Column(db.Boolean, default=False) is_phone_verified = db.Column(db.Boolean, default=False) is_self_sign_up = db.Column(db.Boolean, default=True) is_market_enabled = db.Column(db.Boolean, default=False) password_reset_tokens = db.Column(JSONB, default=[]) pin_reset_tokens = db.Column(JSONB, default=[]) terms_accepted = db.Column(db.Boolean, default=True) matched_profile_pictures = db.Column(JSON) business_usage_id = db.Column(db.Integer, db.ForeignKey(TransferUsage.id)) transfer_accounts = db.relationship( "TransferAccount", secondary=user_transfer_account_association_table, back_populates="users") default_transfer_account_id = db.Column( db.Integer, db.ForeignKey('transfer_account.id'), index=True) default_transfer_account = db.relationship( 'TransferAccount', primaryjoin='TransferAccount.id == User.default_transfer_account_id', lazy=True, uselist=False) default_organisation_id = db.Column(db.Integer, db.ForeignKey('organisation.id'), index=True) default_organisation = db.relationship( 'Organisation', primaryjoin=Organisation.id == default_organisation_id, lazy=True, uselist=False) # roles = db.relationship('UserRole', backref='user', lazy=True, # foreign_keys='UserRole.user_id') ussd_sessions = db.relationship('UssdSession', backref='user', lazy=True, foreign_keys='UssdSession.user_id') uploaded_images = db.relationship('UploadedResource', backref='user', lazy=True, foreign_keys='UploadedResource.user_id') kyc_applications = db.relationship('KycApplication', backref='user', lazy=True, foreign_keys='KycApplication.user_id') devices = db.relationship('DeviceInfo', backref='user', lazy=True) referrals = db.relationship( 'User', secondary=referrals, primaryjoin="User.id == referrals.c.referred_user_id", secondaryjoin="User.id == referrals.c.referrer_user_id", backref='referred_by') transfer_card = db.relationship('TransferCard', backref='user', lazy=True, uselist=False) credit_sends = db.relationship( 'CreditTransfer', backref='sender_user', lazy='dynamic', foreign_keys='CreditTransfer.sender_user_id') credit_receives = db.relationship( 'CreditTransfer', backref='recipient_user', lazy='dynamic', foreign_keys='CreditTransfer.recipient_user_id') ip_addresses = db.relationship('IpAddress', backref='user', lazy=True) feedback = db.relationship('Feedback', backref='user', lazy='dynamic', foreign_keys='Feedback.user_id') custom_attributes = db.relationship( "CustomAttributeUserStorage", backref='user', lazy='joined', foreign_keys='CustomAttributeUserStorage.user_id') exchanges = db.relationship("Exchange", backref="user") @hybrid_property def coordinates(self): return str(self.lat) + ', ' + str(self.lng) @coordinates.expression def coordinates(cls): return cast(cls.lat, String) + ', ' + cast(cls.lng, String) def delete_user_and_transfer_account(self): """ Soft deletes a User and default Transfer account if no other users associated to it. Removes User PII Disables transfer card """ try: ta = self.default_transfer_account ta.delete_transfer_account_from_user(user=self) timenow = datetime.datetime.utcnow() self.deleted = timenow self.first_name = None self.last_name = None self.phone = None transfer_card = None try: transfer_card = TransferCard.get_transfer_card( self.public_serial_number) except NoTransferCardError as e: pass if transfer_card and not transfer_card.is_disabled: transfer_card.disable() except (ResourceAlreadyDeletedError, TransferAccountDeletionError) as e: raise e @hybrid_property def cashout_authorised(self): # loop over all any_valid_token = [t.token for t in self.transfer_accounts] for token in any_valid_token: ct = server.models.credit_transfer example_transfer = ct.CreditTransfer( transfer_type=ct.TransferTypeEnum.PAYMENT, transfer_subtype=ct.TransferSubTypeEnum.AGENT_OUT, sender_user=self, recipient_user=self, token=token, amount=0) limits = example_transfer.get_transfer_limits() limit = limits[0] return limit.period_amount > 0 else: # default to false return False @hybrid_property def phone(self): return self._phone @phone.setter def phone(self, phone): self._phone = proccess_phone_number(phone) @hybrid_property def public_serial_number(self): return self._public_serial_number @public_serial_number.setter def public_serial_number(self, public_serial_number): self._public_serial_number = public_serial_number try: transfer_card = TransferCard.get_transfer_card( public_serial_number) if transfer_card.user_id is None and transfer_card.nfc_serial_number is not None: # Card hasn't been used before, and has a nfc number attached self.nfc_serial_number = transfer_card.nfc_serial_number self.transfer_card = transfer_card except NoTransferCardError: pass @hybrid_property def tfa_url(self): if not self._TFA_secret: self.set_TFA_secret() db.session.flush() secret_key = self.get_TFA_secret() return pyotp.totp.TOTP(secret_key).provisioning_uri( self.email, issuer_name='Sempo: {}'.format( current_app.config.get('DEPLOYMENT_NAME'))) @hybrid_property def location(self): return self._location @location.setter def location(self, location): self._location = location def attempt_update_gps_location(self): from server.utils.location import async_set_user_gps_from_location if self._location is not None and self._location is not '': # Delay execution until after request to avoid race condition with db # We still need to flush to get user id though db.session.flush() add_after_request_executor_job(async_set_user_gps_from_location, kwargs={ 'user_id': self.id, 'location': self._location }) add_after_request_executor_job(async_set_user_gps_from_location, kwargs={ 'user_id': self.id, 'location': self._location }) @hybrid_property def roles(self): if self._held_roles is None: return {} return self._held_roles def remove_all_held_roles(self): self._held_roles = {} def set_held_role(self, role: str, tier: Union[str, None]): if role not in ACCESS_ROLES: raise RoleNotFoundException("Role '{}' not valid".format(role)) allowed_tiers = ACCESS_ROLES[role] if tier is not None and tier not in allowed_tiers: raise TierNotFoundException( "Tier {} not recognised for role {}".format(tier, role)) if self._held_roles is None: self._held_roles = {} if tier is None: self._held_roles.pop(role, None) else: self._held_roles[role] = tier flag_modified(self, '_held_roles') @hybrid_property def has_admin_role(self): return AccessControl.has_any_tier(self.roles, 'ADMIN') @has_admin_role.expression def has_admin_role(cls): return cls._held_roles.has_key('ADMIN') @hybrid_property def has_vendor_role(self): return AccessControl.has_any_tier(self.roles, 'VENDOR') @has_vendor_role.expression def has_vendor_role(cls): return cls._held_roles.has_key('VENDOR') @hybrid_property def has_beneficiary_role(self): return AccessControl.has_any_tier(self.roles, 'BENEFICIARY') @has_beneficiary_role.expression def has_beneficiary_role(cls): return cls._held_roles.has_key('BENEFICIARY') @hybrid_property def has_token_agent_role(self): return AccessControl.has_any_tier(self.roles, 'TOKEN_AGENT') @has_token_agent_role.expression def has_token_agent_role(cls): return cls._held_roles.has_key('TOKEN_AGENT') @hybrid_property def has_group_account_role(self): return AccessControl.has_any_tier(self.roles, 'GROUP_ACCOUNT') @has_group_account_role.expression def has_group_account_role(cls): return cls._held_roles.has_key('GROUP_ACCOUNT') @hybrid_property def admin_tier(self): return self._held_roles.get('ADMIN', None) @hybrid_property def vendor_tier(self): return self._held_roles.get('VENDOR', None) # todo: Refactor into above roles # These two are here to interface with the mobile API @hybrid_property def is_vendor(self): return AccessControl.has_sufficient_tier(self.roles, 'VENDOR', 'vendor') @hybrid_property def is_supervendor(self): return AccessControl.has_sufficient_tier(self.roles, 'VENDOR', 'supervendor') @hybrid_property def organisation_ids(self): return [organisation.id for organisation in self.organisations] @property def transfer_account(self): active_organisation = getattr( g, "active_organisation", None) or self.fallback_active_organisation() # TODO: Review if this could have a better concept of a default? return self.get_transfer_account_for_organisation(active_organisation) @hybrid_method def great_circle_distance(self, lat, lng): """ Tries to calculate the great circle distance between the two locations in km by using the Haversine formula. """ return self._haversine(math, self, lat, lng) @great_circle_distance.expression def great_circle_distance(cls, lat, lng): return cls._haversine(func, cls, lat, lng) @staticmethod def _haversine(lib, selfref, lat, lng): return 6371 * lib.acos( lib.cos(lib.radians(selfref.lat)) * lib.cos(lib.radians(lat)) * lib.cos(lib.radians(selfref.lng) - lib.radians(lng)) + lib.sin(lib.radians(selfref.lat)) * lib.sin(lib.radians(lat))) def get_users_within_radius(self, radius): if not (self.lat or self.lng): raise Exception( 'Cannot get users within radius-- User location undefined') return db.session.query(User).filter( self.users_within_radius_filter(radius)).all() def users_within_radius_filter(self, radius): return or_( and_(User.lat == None, User.lng == None), and_(User.lat == self.lat, User.lng == self.lng), User.great_circle_distance(self.lat, self.lng) < radius, and_(self._location is not None, User._location == self._location)) def get_transfer_account_for_organisation(self, organisation): for ta in self.transfer_accounts: if ta.organisation.id == organisation.id: return ta raise Exception( f"No matching transfer account for user {self}, token {organisation.token} and organsation {organisation}" ) def get_transfer_account_for_token(self, token): return find_transfer_accounts_with_matching_token(self, token) def fallback_active_organisation(self): if len(self.organisations) == 0: return None if len(self.organisations) > 1: return self.default_organisation return self.organisations[0] def update_last_seen_ts(self): pass # cur_time = datetime.datetime.utcnow() # if self._last_seen: # # default to 1 minute intervals # if cur_time - self._last_seen >= datetime.timedelta(minutes=1): # self._last_seen = cur_time # else: # self._last_seen = cur_time @staticmethod def salt_hash_secret(password): f = Fernet(config.PASSWORD_PEPPER) return f.encrypt(bcrypt.hashpw(password.encode(), bcrypt.gensalt())).decode() @staticmethod def check_salt_hashed_secret(password, hashed_password): if not hashed_password: return False f = Fernet(config.PASSWORD_PEPPER) hashed_password = f.decrypt(hashed_password.encode()) return bcrypt.checkpw(password.encode(), hashed_password) def hash_password(self, password): self.password_hash = self.salt_hash_secret(password) def verify_password(self, password): return self.check_salt_hashed_secret(password, self.password_hash) def hash_pin(self, pin): self.pin_hash = self.salt_hash_secret(pin) def verify_pin(self, pin): return self.check_salt_hashed_secret(pin, self.pin_hash) def encode_TFA_token(self, valid_days=1): """ Generates the Auth Token for TFA :return: string """ try: payload = { 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=valid_days, seconds=30), 'iat': datetime.datetime.utcnow(), 'id': self.id } tfa = jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256') return bytes(tfa, 'utf-8') if isinstance(tfa, str) else tfa except Exception as e: return e def encode_auth_token(self): """ Generates the Auth Token :return: string """ try: payload = { 'exp': datetime.datetime.utcnow() + datetime.timedelta( seconds=current_app.config['AUTH_TOKEN_EXPIRATION']), 'iat': datetime.datetime.utcnow(), 'id': self.id, 'roles': self.roles } token = jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256') return bytes(token, 'utf-8') if isinstance(token, str) else token except Exception as e: return e @staticmethod def decode_auth_token(auth_token, token_type='Auth'): """ Validates the auth token :param auth_token: :return: integer|string """ try: payload = jwt.decode(auth_token, current_app.config['SECRET_KEY'], algorithms='HS256', options={ 'verify_exp': current_app.config['VERIFY_JWT_EXPIRY'] }) is_blacklisted_token = BlacklistToken.check_blacklist(auth_token) if is_blacklisted_token: return 'Token blacklisted. Please log in again.' else: return bytes(payload, 'utf-8') if isinstance(payload, str) else payload except jwt.ExpiredSignatureError: return '{} Token Signature expired.'.format(token_type) except jwt.InvalidTokenError: return 'Invalid {} Token.'.format(token_type) def encode_single_use_JWS(self, token_type): s = TimedJSONWebSignatureSerializer( current_app.config['SECRET_KEY'], expires_in=current_app.config['SINGLE_USE_TOKEN_EXPIRATION']) return s.dumps({'id': self.id, 'type': token_type}).decode("utf-8") @classmethod def decode_single_use_JWS(cls, token, required_type): try: s = TimedJSONWebSignatureSerializer( current_app.config['SECRET_KEY']) data = s.loads(token.encode("utf-8")) user_id = data.get('id') token_type = data.get('type') if token_type != required_type: return { 'success': False, 'message': 'Wrong token type (needed %s)' % required_type } if not user_id: return {'success': False, 'message': 'No User ID provided'} user = cls.query.filter_by(id=user_id).execution_options( show_all=True).first() if not user: return {'success': False, 'message': 'User not found'} return {'success': True, 'user': user} except BadSignature: return {'success': False, 'message': 'Token signature not valid'} except SignatureExpired: return {'success': False, 'message': 'Token has expired'} except Exception as e: return {'success': False, 'message': e} def save_password_reset_token(self, password_reset_token): # make a "clone" of the existing token list self.clear_expired_password_reset_tokens() current_password_reset_tokens = self.password_reset_tokens[:] current_password_reset_tokens.append(password_reset_token) # set db value self.password_reset_tokens = current_password_reset_tokens def save_pin_reset_token(self, pin_reset_token): self.clear_expired_pin_reset_tokens() current_pin_reset_tokens = self.pin_reset_tokens[:] current_pin_reset_tokens.append(pin_reset_token) self.pin_reset_tokens = current_pin_reset_tokens def check_reset_token_already_used(self, password_reset_token): self.clear_expired_password_reset_tokens() is_valid = password_reset_token in self.password_reset_tokens return is_valid def delete_password_reset_tokens(self): self.password_reset_tokens = [] def delete_pin_reset_tokens(self): self.pin_reset_tokens = [] def clear_expired_reset_tokens(self, token_list): if token_list is None: token_list = [] valid_tokens = [] for token in token_list: validity_check = self.decode_single_use_JWS(token, 'R') if validity_check['success']: valid_tokens.append(token) return valid_tokens def clear_expired_password_reset_tokens(self): tokens = self.clear_expired_reset_tokens(self.password_reset_tokens) self.password_reset_tokens = tokens def clear_expired_pin_reset_tokens(self): tokens = self.clear_expired_reset_tokens(self.pin_reset_tokens) self.pin_reset_tokens = tokens def create_admin_auth(self, email, password, tier='view', organisation=None): self.email = email self.hash_password(password) self.set_held_role('ADMIN', tier) if organisation: self.add_user_to_organisation(organisation, is_admin=True) def add_user_to_organisation(self, organisation: Organisation, is_admin=False): if not self.default_organisation: self.default_organisation = organisation self.organisations.append(organisation) if is_admin and organisation.org_level_transfer_account_id: if organisation.org_level_transfer_account is None: organisation.org_level_transfer_account = (db.session.query( server.models.transfer_account.TransferAccount ).execution_options(show_all=True).get( organisation.org_level_transfer_account_id)) self.transfer_accounts.append( organisation.org_level_transfer_account) def is_TFA_required(self): for tier in current_app.config['TFA_REQUIRED_ROLES']: if AccessControl.has_exact_role(self.roles, 'ADMIN', tier): return True else: return False def is_TFA_secret_set(self): return bool(self._TFA_secret) def set_TFA_secret(self): secret = pyotp.random_base32() self._TFA_secret = encrypt_string(secret) def get_TFA_secret(self): return decrypt_string(self._TFA_secret) def validate_OTP(self, input_otp): secret = self.get_TFA_secret() server_otp = pyotp.TOTP(secret) ret = server_otp.verify(input_otp, valid_window=2) return ret def set_one_time_code(self, supplied_one_time_code): if supplied_one_time_code is None: self.one_time_code = str(random.randint(0, 9999)).zfill(4) else: self.one_time_code = supplied_one_time_code # pin as used in mobile. is set to password. we should probably change this to be same as ussd pin def set_pin(self, supplied_pin=None, is_activated=False): self.is_activated = is_activated if not is_activated: # Use a one time code, either generated or supplied. PIN will be set to random number for now self.set_one_time_code(supplied_one_time_code=supplied_pin) pin = str(random.randint(0, 9999)).zfill(4) else: pin = supplied_pin self.hash_pin(pin) def has_valid_pin(self): # not in the process of resetting pin and has a pin self.clear_expired_pin_reset_tokens() not_resetting = len(self.pin_reset_tokens) == 0 return self.pin_hash is not None and not_resetting and self.failed_pin_attempts < 3 def user_details(self): # should drop the country code from phone number? return "{} {} {}".format(self.first_name, self.last_name, self.phone) def get_most_relevant_transfer_usages(self): '''Finds the transfer usage/business categories there are most relevant for the user based on the last number of send and completed credit transfers supplemented with the defaul business categories :return: list of most relevant transfer usage objects for the usage """ ''' sql = text(''' SELECT *, COUNT(*) FROM (SELECT c.transfer_use::text FROM credit_transfer c WHERE c.sender_user_id = {} AND c.transfer_status = 'COMPLETE' ORDER BY c.updated DESC LIMIT 20) C GROUP BY transfer_use ORDER BY count DESC '''.format(self.id)) result = db.session.execute(sql) most_common_uses = {} for row in result: if row[0] is not None: for use in json.loads(row[0]): most_common_uses[use] = row[1] return most_common_uses def get_reserve_token(self): # reserve token is master token for now return Organisation.master_organisation().token def __init__(self, blockchain_address=None, **kwargs): super(User, self).__init__(**kwargs) self.secret = ''.join( random.choices(string.ascii_letters + string.digits, k=16)) self.primary_blockchain_address = blockchain_address or bt.create_blockchain_wallet( ) def __repr__(self): if self.has_admin_role: return '<Admin {} {}>'.format(self.id, self.email) elif self.has_vendor_role: return '<Vendor {} {}>'.format(self.id, self.phone) else: return '<User {} {}>'.format(self.id, self.phone)
class User(ModelBase): """Establishes the identity of a user for both making transactions and more general interactions. Admin users are created through the auth api by registering an account with an email that has been pre-approved on the whitelist. By default, admin users only have minimal access levels (view). Permissions must be elevated manually in the database. Transaction-capable users (vendors and beneficiaries) are created using the POST user API or the bulk upload function """ __tablename__ = 'user' first_name = db.Column(db.String()) last_name = db.Column(db.String()) _last_seen = db.Column(db.DateTime) email = db.Column(db.String()) _phone = db.Column(db.String()) _public_serial_number = db.Column(db.String()) nfc_serial_number = db.Column(db.String()) password_hash = db.Column(db.String(128)) one_time_code = db.Column(db.String) secret = db.Column(db.String()) _TFA_secret = db.Column(db.String(128)) TFA_enabled = db.Column(db.Boolean, default=False) default_currency = db.Column(db.String()) _location = db.Column(db.String()) lat = db.Column(db.Float()) lng = db.Column(db.Float()) is_beneficiary = db.Column(db.Boolean, default=False) _is_vendor = db.Column(db.Boolean, default=False) _is_supervendor = db.Column(db.Boolean, default=False) _is_view = db.Column(db.Boolean, default=False) _is_subadmin = db.Column(db.Boolean, default=False) _is_admin = db.Column(db.Boolean, default=False) _is_superadmin = db.Column(db.Boolean, default=False) is_activated = db.Column(db.Boolean, default=False) is_disabled = db.Column(db.Boolean, default=False) terms_accepted = db.Column(db.Boolean, default=True) custom_attributes = db.Column(JSON) matched_profile_pictures = db.Column(JSON) ap_user_id = db.Column(db.String()) ap_bank_id = db.Column(db.String()) ap_paypal_id = db.Column(db.String()) kyc_state = db.Column(db.String()) cashout_authorised = db.Column(db.Boolean, default=False) transfer_account_id = db.Column(db.Integer, db.ForeignKey('transfer_account.id')) chatbot_state_id = db.Column(db.Integer, db.ForeignKey('chatbot_state.id')) targeting_survey_id = db.Column(db.Integer, db.ForeignKey('targeting_survey.id')) uploaded_images = db.relationship('UploadedImage', backref='user', lazy=True, foreign_keys='UploadedImage.user_id') devices = db.relationship('DeviceInfo', backref='user', lazy=True) referrals = db.relationship('Referral', backref='referring_user', lazy=True) transfer_card = db.relationship('TransferCard', backref='user', lazy=True, uselist=False) credit_sends = db.relationship( 'CreditTransfer', backref='sender_user', lazy='dynamic', foreign_keys='CreditTransfer.sender_user_id') credit_receives = db.relationship( 'CreditTransfer', backref='recipient_user', lazy='dynamic', foreign_keys='CreditTransfer.recipient_user_id') ip_addresses = db.relationship('IpAddress', backref='user', lazy=True) @hybrid_property def phone(self): return self._phone @phone.setter def phone(self, phone): self._phone = proccess_phone_number(phone) @hybrid_property def public_serial_number(self): return self._public_serial_number @public_serial_number.setter def public_serial_number(self, public_serial_number): self._public_serial_number = public_serial_number try: transfer_card = get_transfer_card(public_serial_number) if transfer_card.user_id is None and transfer_card.nfc_serial_number is not None: # Card hasn't been used before, and has a nfc number attached self.nfc_serial_number = transfer_card.nfc_serial_number self.transfer_card = transfer_card except NoTransferCardError: pass @hybrid_property def tfa_url(self): if not self._TFA_secret: self._set_TFA_secret() db.session.commit() secret_key = self._get_TFA_secret() return pyotp.totp.TOTP(secret_key).provisioning_uri( self.email, issuer_name='Sempo: {}'.format( current_app.config.get('DEPLOYMENT_NAME'))) @hybrid_property def location(self): return self._location @location.setter def location(self, location): self._location = location if location is not None and location is not '': try: task = {'user_id': self.id, 'address': location} geolocate_task = celery_app.signature( 'worker.celery_tasks.geolocate_address', args=(task, )) geolocate_task.delay() except Exception as e: print(e) sentry.captureException() pass @hybrid_property def has_any_admin_role(self): return self._is_view or self._is_subadmin or self._is_admin or self._is_superadmin @hybrid_property def has_any_vendor_role(self): return self._is_vendor or self._is_supervendor @hybrid_property def is_vendor(self): return self._is_vendor or self._is_supervendor @is_vendor.setter def is_vendor(self, is_vendor): self._is_vendor = is_vendor @hybrid_property def is_supervendor(self): return self._is_supervendor @is_supervendor.setter def is_supervendor(self, is_supervendor): self._is_supervendor = is_supervendor @hybrid_property def is_view(self): return self._is_view or self._is_subadmin or self._is_admin or self._is_superadmin @is_view.setter def is_view(self, is_view): self._is_view = is_view @hybrid_property def is_subadmin(self): return self._is_subadmin or self._is_admin or self._is_superadmin @is_subadmin.setter def is_subadmin(self, is_subadmin): self._is_subadmin = is_subadmin @hybrid_property def is_admin(self): return self._is_admin or self._is_superadmin @is_admin.setter def is_admin(self, is_admin): self._is_admin = is_admin @hybrid_property def is_superadmin(self): return self._is_superadmin @is_superadmin.setter def is_superadmin(self, is_superadmin): self._is_superadmin = is_superadmin def update_last_seen_ts(self): cur_time = datetime.datetime.utcnow() if self._last_seen: if cur_time - self._last_seen >= datetime.timedelta( minutes=1): # default to 1 minute intervals self._last_seen = cur_time else: self._last_seen = cur_time def hash_password(self, password): self.password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() def verify_password(self, password): return bcrypt.checkpw(password.encode(), self.password_hash.encode()) def encode_TFA_token(self, valid_days=1): """ Generates the Auth Token for TFA :return: string """ try: payload = { 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=valid_days, seconds=30), 'iat': datetime.datetime.utcnow(), 'user_id': self.id } return jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256') except Exception as e: return e def encode_auth_token(self): """ Generates the Auth Token :return: string """ try: payload = { 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7, seconds=0), 'iat': datetime.datetime.utcnow(), 'user_id': self.id, 'is_vendor': self.is_vendor, 'is_supervendor': self.is_supervendor, 'is_view': self.is_view, 'is_subadmin': self.is_subadmin, 'is_admin': self.is_admin, 'is_superadmin': self.is_superadmin } return jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256') except Exception as e: return e @staticmethod def decode_auth_token(auth_token): """ Validates the auth token :param auth_token: :return: integer|string """ try: payload = jwt.decode(auth_token, current_app.config.get('SECRET_KEY')) is_blacklisted_token = BlacklistToken.check_blacklist(auth_token) if is_blacklisted_token: return 'Token blacklisted. Please log in again.' else: return payload except jwt.ExpiredSignatureError: return 'Signature expired.' except jwt.InvalidTokenError: return 'Invalid token.' def encode_single_use_JWS(self, token_type): s = TimedJSONWebSignatureSerializer( current_app.config['SECRET_KEY'], expires_in=current_app.config['TOKEN_EXPIRATION']) return s.dumps({'id': self.id, 'type': token_type}).decode("utf-8") @classmethod def decode_single_use_JWS(cls, token, required_type): try: s = TimedJSONWebSignatureSerializer( current_app.config['SECRET_KEY']) data = s.loads(token.encode("utf-8")) user_id = data.get('id') token_type = data.get('type') if token_type != required_type: return { 'success': False, 'message': 'Wrong token type (needed %s)' % required_type } if not user_id: return {'success': False, 'message': 'No User ID provided'} user = cls.query.filter_by(id=user_id).first() if not user: return {'success': False, 'message': 'User not found'} return {'success': True, 'user': user} except BadSignature: return {'success': False, 'message': 'Token signature not valid'} except SignatureExpired: return {'success': False, 'message': 'Token has expired'} except Exception as e: return {'success': False, 'message': e} def create_admin_auth(self, email, password, tier='view'): self.email = email self.hash_password(password) self.set_admin_role_using_tier_string(tier) def set_admin_role_using_tier_string(self, tier): tier = tier.lower() if tier not in ALLOWED_ADMIN_TIERS: raise TierNotFoundException('tier {} not found') self.is_view = self.is_subadmin = self.is_admin = self.is_superadmin = False if tier == 'superadmin': self.is_superadmin = True elif tier == 'admin': self.is_admin = True elif tier == 'subadmin': self.is_subadmin = True elif tier == 'view': self.is_view = True if self.is_admin: return 'admin' def convert_user_role_to_string(self): user_role = "" if self.is_superadmin: user_role = 'superadmin' elif self.is_admin: user_role = 'admin' elif self.is_subadmin: user_role = 'subadmin' elif self.is_view: user_role = 'view' if user_role in ALLOWED_ADMIN_TIERS: return user_role else: return "" def is_TFA_required(self): for role in current_app.config['TFA_REQUIRED_ROLES']: if self.convert_user_role_to_string() == role: return True else: return False def is_TFA_secret_set(self): return bool(self._TFA_secret) def _set_TFA_secret(self): secret = pyotp.random_base32() self._TFA_secret = encrypt_string(secret) def _get_TFA_secret(self): return decrypt_string(self._TFA_secret) def validate_OTP(self, input_otp): try: p = int(input_otp) except ValueError: return False else: secret = self._get_TFA_secret() server_otp = pyotp.TOTP(secret) ret = server_otp.verify(p) return ret def set_pin(self, supplied_pin=None, is_activated=False): self.is_activated = is_activated if not is_activated: # Use a one time code, either generated or supplied. PIN will be set to random number for now if supplied_pin is None: self.one_time_code = str(random.randint(0, 9999)).zfill(4) else: self.one_time_code = supplied_pin pin = str(random.randint(0, 9999999999999)).zfill(4) else: pin = supplied_pin self.hash_password(pin) def set_non_admin_auth(self, is_beneficiary=False, is_vendor=False, is_supervendor=False): self.is_vendor = is_vendor self.is_supervendor = is_supervendor self.is_beneficiary = is_beneficiary def __init__(self, **kwargs): super(User, self).__init__(**kwargs) self.secret = ''.join( random.choices(string.ascii_letters + string.digits, k=16)) def __repr__(self): if self.is_view: return '<Admin {} {}>'.format(self.id, self.email) elif self.is_vendor: return '<Vendor {} {}>'.format(self.id, self.phone) else: return '<Beneficiary {} {}>'.format(self.id, self.phone)
class Organisation(ModelBase): """ Establishes organisation object that resources can be associated with. """ __tablename__ = 'organisation' is_master = db.Column(db.Boolean, default=False, index=True) name = db.Column(db.String) external_auth_username = db.Column(db.String) valid_roles = db.Column(ARRAY(db.String, dimensions=1)) _external_auth_password = db.Column(db.String) default_lat = db.Column(db.Float()) default_lng = db.Column(db.Float()) _timezone = db.Column(db.String) _country_code = db.Column(db.String, nullable=False) _default_disbursement_wei = db.Column(db.Numeric(27), default=0) require_transfer_card = db.Column(db.Boolean, default=False) # TODO: Create a mixin so that both user and organisation can use the same definition here # This is the blockchain address used for transfer accounts, unless overridden primary_blockchain_address = db.Column(db.String) # This is the 'behind the scenes' blockchain address used for paying gas fees system_blockchain_address = db.Column(db.String) auto_approve_externally_created_users = db.Column(db.Boolean, default=False) users = db.relationship( "User", secondary=organisation_association_table, back_populates="organisations") token_id = db.Column(db.Integer, db.ForeignKey('token.id')) org_level_transfer_account_id = db.Column(db.Integer, db.ForeignKey('transfer_account.id', name="fk_org_level_account")) # We use this weird join pattern because SQLAlchemy # doesn't play nice when doing multiple joins of the same table over different declerative bases org_level_transfer_account = db.relationship( "TransferAccount", post_update=True, primaryjoin="Organisation.org_level_transfer_account_id==TransferAccount.id", uselist=False) @hybrid_property def timezone(self): return self._timezone @timezone.setter def timezone(self, val): if val is not None and val not in pendulum.timezones: raise Exception(f"{val} is not a valid timezone") self._timezone = val @hybrid_property def country_code(self): return self._country_code @country_code.setter def country_code(self, val): if val is not None: val = val.upper() if len(val) != 2: # will try handle 'AD: Andorra' val = val.split(':')[0] if val not in ISO_COUNTRIES: raise Exception(f"{val} is not a valid country code") self._country_code = val @property def default_disbursement(self): return Decimal((self._default_disbursement_wei or 0) / int(1e16)) @default_disbursement.setter def default_disbursement(self, val): if val is not None: self._default_disbursement_wei = int(val) * int(1e16) # TODO: This is a hack to get around the fact that org level TAs don't always show up. Super not ideal @property def queried_org_level_transfer_account(self): if self.org_level_transfer_account_id: return server.models.transfer_account.TransferAccount\ .query.execution_options(show_all=True).get(self.org_level_transfer_account_id) return None @hybrid_property def external_auth_password(self): return decrypt_string(self._external_auth_password) @external_auth_password.setter def external_auth_password(self, value): self._external_auth_password = encrypt_string(value) credit_transfers = db.relationship("CreditTransfer", secondary=organisation_association_table, back_populates="organisations") transfer_accounts = db.relationship('TransferAccount', backref='organisation', lazy=True, foreign_keys='TransferAccount.organisation_id') blockchain_addresses = db.relationship('BlockchainAddress', backref='organisation', lazy=True, foreign_keys='BlockchainAddress.organisation_id') email_whitelists = db.relationship('EmailWhitelist', backref='organisation', lazy=True, foreign_keys='EmailWhitelist.organisation_id') kyc_applications = db.relationship('KycApplication', backref='organisation', lazy=True, foreign_keys='KycApplication.organisation_id') attribute_maps = db.relationship('AttributeMap', backref='organisation', lazy=True, foreign_keys='AttributeMap.organisation_id') custom_welcome_message_key = db.Column(db.String) @staticmethod def master_organisation() -> "Organisation": return Organisation.query.filter_by(is_master=True).first() def _setup_org_transfer_account(self): transfer_account = server.models.transfer_account.TransferAccount( bound_entity=self, is_approved=True ) db.session.add(transfer_account) self.org_level_transfer_account = transfer_account # Back setup for delayed organisation transfer account instantiation for user in self.users: if AccessControl.has_any_tier(user.roles, 'ADMIN'): user.transfer_accounts.append(self.org_level_transfer_account) def bind_token(self, token): self.token = token self._setup_org_transfer_account() def __init__(self, token=None, is_master=False, valid_roles=None, **kwargs): super(Organisation, self).__init__(**kwargs) self.external_auth_username = '******'+ self.name.lower().replace(' ', '_') self.external_auth_password = secrets.token_hex(16) self.valid_roles = valid_roles or list(ASSIGNABLE_TIERS.keys()) if is_master: if Organisation.query.filter_by(is_master=True).first(): raise Exception("A master organisation already exists") self.is_master = True self.system_blockchain_address = bt.create_blockchain_wallet( private_key=current_app.config['MASTER_WALLET_PRIVATE_KEY'], wei_target_balance=0, wei_topup_threshold=0, ) self.primary_blockchain_address = self.system_blockchain_address or bt.create_blockchain_wallet() else: self.is_master = False self.system_blockchain_address = bt.create_blockchain_wallet( wei_target_balance=current_app.config['SYSTEM_WALLET_TARGET_BALANCE'], wei_topup_threshold=current_app.config['SYSTEM_WALLET_TOPUP_THRESHOLD'], ) self.primary_blockchain_address = bt.create_blockchain_wallet() if token: self.bind_token(token)
class Post(db.Model): __tablename__ = "posts" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(40), nullable=False) imageurl = db.Column(db.String(128)) condition = db.Column(db.String(16), nullable=False) description = db.Column(db.String(140), nullable=False) price = db.Column(db.Float(2), nullable=False) userid = db.Column(db.Integer, db.ForeignKey("users.id")) schoolid = db.Column(db.Integer, db.ForeignKey("schools.id")) user = db.relationship("User") school = db.relationship("School") def __init__(self, title, condition, description, price, userid, schoolid, imageurl=None): self.title = title self.imageurl = imageurl self.condition = condition self.description = description self.price = price self.userid = userid self.schoolid = schoolid def add(self): db.session.add(self) db.session.commit() @property def serialize(self): return { "title": self.title, "image": self.image_url, "condition": self.condition, "description": self.description, "price": self.price, "user": self.user.firstName, "email": self.user.email } @staticmethod def fromForm(form): title = form.title.data imageurl = form.imageurl.data condition = form.condition.data description = form.description.data price = form.price.data userid = form.userid.data schoolid = form.schoolid.data return Post(title, condition, description, price, userid, schoolid, imageurl=imageurl)