def test_edit_user(test_client, authed_sempo_admin_user, create_transfer_account_user, user_id_accessor, is_vendor, is_groupaccount, roles, tier, status_code): if tier: authed_sempo_admin_user.set_held_role('ADMIN', tier) auth = get_complete_auth_token(authed_sempo_admin_user) else: auth = None new_phone = fake.msisdn() response = test_client.put( f"/api/v1/user/{user_id_accessor(create_transfer_account_user)}/", headers=dict( Authorization=auth, Accept='application/json' ), json={ 'phone': new_phone, 'account_types': roles }) assert response.status_code == status_code if response.status_code == 200: data = response.json['data'] assert isinstance(data['user'], object) assert data['user']['phone'] == proccess_phone_number(new_phone) assert data['user']['is_vendor'] == is_vendor assert data['user']['is_groupaccount'] == is_groupaccount
def test_proccess_phone_number(proccess_phone_number, phone, region, expected): """ GIVEN proccess_phone_number function WHEN called with a phone_number WITHOUT and WITH country code THEN check that default country code is added if required """ assert proccess_phone_number(phone, region) == expected
def find_user_from_public_identifier(*public_identifiers): """ :param public_identifiers: email, phone, public_serial_number, nfc_serial_number or address :return: First user found """ user = None transfer_card = None for public_identifier in list(filter(lambda x: x is not None, public_identifiers)): if public_identifier is None: continue user = User.query.execution_options(show_all=True).filter_by( email=str(public_identifier).lower()).first() if user: break try: user = User.query.execution_options(show_all=True).filter_by( phone=proccess_phone_number(public_identifier)).first() if user: break except NumberParseException: pass transfer_card = TransferCard.query.execution_options(show_all=True).filter_by( public_serial_number=str(public_identifier).lower()).first() user = transfer_card and transfer_card.user if user: break transfer_card = TransferCard.query.execution_options(show_all=True).filter_by( nfc_serial_number=public_identifier.upper()).first() user = transfer_card and transfer_card.user if user: break user = User.query.execution_options(show_all=True).filter_by( uuid=public_identifier).first() if user: break try: checksummed = to_checksum_address(public_identifier) blockchain_address = BlockchainAddress.query.filter_by( address=checksummed).first() if blockchain_address and blockchain_address.transfer_account: user = blockchain_address.transfer_account.primary_user if user: break except Exception: pass return user, transfer_card
def test_create_user(test_client, authed_sempo_admin_user, init_database, create_transfer_account_user, mock_async_set_user_gps_from_location, user_phone_accessor, phone, business_usage_name, referred_by, tier, status_code): if tier: authed_sempo_admin_user.set_held_role('ADMIN', tier) auth = get_complete_auth_token(authed_sempo_admin_user) else: auth = None # create the user who is referring create_transfer_account_user.phone = referred_by user_phone_accessor(create_transfer_account_user) response = test_client.post( "/api/v1/user/", headers=dict( Authorization=auth, Accept='application/json' ), json={ 'first_name': 'John', 'last_name': 'Smith', 'bio': 'EasyMart', 'gender': 'female', 'phone': phone, 'is_vendor': False, 'is_tokenagent': False, 'is_groupaccount': False, 'initial_disbursement': 0, 'location': 'Elwood', 'business_usage_name': business_usage_name, 'referred_by': user_phone_accessor(create_transfer_account_user) }) assert response.status_code == status_code if response.status_code == 200: data = response.json['data'] assert isinstance(data['user'], object) assert data['user']['first_name'] == 'John' assert data['user']['last_name'] == 'Smith' assert data['user']['custom_attributes']['bio'] == 'EasyMart' assert data['user']['custom_attributes']['gender'] == 'female' assert data['user']['phone'] == proccess_phone_number(phone) assert data['user']['is_vendor'] is False assert data['user']['is_tokenagent'] is False assert data['user']['is_groupaccount'] is False assert data['user']['transfer_accounts'][0]['balance'] == 0 assert data['user']['location'] == 'Elwood' assert data['user']['business_usage_id'] == init_database.session.query(TransferUsage)\ .filter_by(name=business_usage_name).first().id assert data['user']['referred_by'] == user_phone_accessor(create_transfer_account_user) # Checks that we're calling the gps location fetching job, and passing the right data to it # Used in lieu of the test below working fn_inputs = mock_async_set_user_gps_from_location args, kwargs = fn_inputs[-1] assert kwargs == {'user_id': data['user']['id'], 'location': 'Elwood'}
def get_user_by_phone(phone: str, region: str, should_raise=False) -> Optional[User]: user = User.query.execution_options(show_all=True).filter_by( phone=proccess_phone_number(phone_number=phone, region=region)).first() if user is not None: return user else: if should_raise: raise Exception('User not found.') else: return None
def upsell_unregistered_recipient(self, user_input): recipient_phone = proccess_phone_number(user_input) self.send_sms( self.user.phone, 'upsell_message_sender', recipient_phone=recipient_phone, ) self.send_sms( recipient_phone, 'upsell_message_recipient', first_name=self.user.first_name, last_name=self.user.last_name, token_name=default_token(self.user).name )
def find_user_from_public_identifier(*public_identifiers): user = None for public_identifier in public_identifiers: if public_identifier is None: continue user = models.User.query.filter_by( email=str(public_identifier).lower()).first() if user: continue try: user = models.User.query.filter_by( phone=proccess_phone_number(public_identifier)).first() if user: continue except NumberParseException: pass user = models.User.query.filter_by( public_serial_number=str(public_identifier).lower()).first() if user: continue user = models.User.query.filter_by( nfc_serial_number=public_identifier.upper()).first() if user: continue try: checksummed = utils.checksum_encode(public_identifier) blockchain_address = models.BlockchainAddress.query.filter_by( address=checksummed).first() if blockchain_address and blockchain_address.transfer_account: user = blockchain_address.transfer_account.primary_user if user: continue except Exception: pass return user
def __init__(self, inbound_phone, inbound_message, message_source=None, provider_message_id=None): self.inbound_phone = proccess_phone_number(inbound_phone) self.inbound_message = inbound_message.lower() self.inbound_user = User.query.filter_by( phone=self.inbound_phone).first() self.inbound_transfer_account = self.inbound_user.transfer_account self.chatbot_state = self.inbound_user.chatbot_state self.provider_message_id = provider_message_id self.message_source = message_source
def test_create_user(test_client, authed_sempo_admin_user, init_database, create_transfer_account_user, user_phone_accessor, phone, business_usage_name, referred_by, tier, status_code): if tier: authed_sempo_admin_user.set_held_role('ADMIN', tier) auth = get_complete_auth_token(authed_sempo_admin_user) else: auth = None response = test_client.post( "/api/v1/user/", headers=dict(Authorization=auth, Accept='application/json'), json={ 'first_name': 'John', 'last_name': 'Smith', 'bio': 'EasyMart', 'gender': 'female', 'phone': phone, 'is_vendor': False, 'is_tokenagent': False, 'is_groupaccount': False, 'initial_disbursement': 0, 'location': 'Elwood', 'business_usage_name': business_usage_name, 'referred_by': user_phone_accessor(create_transfer_account_user ) #create the user who is referring }) assert response.status_code == status_code if response.status_code == 200: data = response.json['data'] assert isinstance(data['user'], object) assert data['user']['first_name'] == 'John' assert data['user']['last_name'] == 'Smith' assert data['user']['custom_attributes']['bio'] == 'EasyMart' assert data['user']['custom_attributes']['gender'] == 'female' assert data['user']['phone'] == proccess_phone_number(phone) assert data['user']['is_vendor'] is False assert data['user']['is_tokenagent'] is False assert data['user']['is_groupaccount'] is False assert data['user']['transfer_accounts'][0]['balance'] == 0 assert data['user']['location'] == 'Elwood' assert data['user']['business_usage_id'] == init_database.session.query(TransferUsage)\ .filter_by(name=business_usage_name).first().id
def bind_fb_psid_to_account(message, psid): if message == "bind demo pin 4563": transfer_account = User.query.get(5) transfer_account.facebook_psid = str(psid) db.session.commit() return 'Bound to demo account' try: index_of_use = message.lower().index('use') index_of_pin = message.lower().index('pin') phone_number = proccess_phone_number(message[index_of_use + 3:index_of_pin]) pin = str(int(message[index_of_pin + 3:])) except: return 'Please link to your Sempo account first' user = User.query.filter_by(phone=phone_number).first() if user is None: return 'phone number not registered' if user.verify_password(pin): return 'wrong pin' user.facebook_psid = str(psid) db.session.commit() return 'Sempo account linked'
def post(self): # get the post data post_data = request.get_json() old_password = post_data.get('old_password') new_password = post_data.get('new_password') phone = proccess_phone_number(phone_number=post_data.get('phone'), region=post_data.get('region')) one_time_code = post_data.get('one_time_code') auth_header = request.headers.get('Authorization') # Check authorisation using a one time code if phone and one_time_code: card = phone[-6:] user = (User.query.filter_by(phone=phone).execution_options(show_all=True).first() or User.query.filter_by(public_serial_number=card).execution_options(show_all=True).first() ) if not user: response_object = { 'status': 'fail', 'message': 'User not found' } return make_response(jsonify(response_object)), 401 if user.is_activated: response_object = { 'status': 'fail', 'message': 'Account already activated' } return make_response(jsonify(response_object)), 401 if str(one_time_code) != user.one_time_code: response_object = { 'status': 'fail', 'message': 'One time code not valid' } return make_response(jsonify(response_object)), 401 user.hash_password(new_password) user.is_phone_verified = True user.is_activated = True user.one_time_code = None auth_token = user.encode_auth_token() response_object = create_user_response_object(user, auth_token, 'Successfully set pin') db.session.commit() return make_response(jsonify(response_object)), 200 # Check authorisation using regular auth elif auth_header and auth_header != 'null' and old_password: auth_token = auth_header.split(" ")[0] resp = User.decode_auth_token(auth_token) if isinstance(resp, str): response_object = { 'status': 'fail', 'message': 'Invalid auth token' } return make_response(jsonify(response_object)), 401 user = User.query.filter_by(id=resp.get('user_id')).execution_options(show_all=True).first() if not user: response_object = { 'status': 'fail', 'message': 'User not found' } return make_response(jsonify(response_object)), 401 if not user.verify_password(old_password): response_object = { 'status': 'fail', 'message': 'invalid password' } return make_response(jsonify(response_object)), 401 # Check authorisation using a reset token provided via email else: reset_password_token = post_data.get('reset_password_token') if not reset_password_token: response_object = { 'status': 'fail', 'message': 'Missing token.' } return make_response(jsonify(response_object)), 401 reset_password_token = reset_password_token.split(" ")[0] validity_check = User.decode_single_use_JWS(reset_password_token, 'R') if not validity_check['success']: response_object = { 'status': 'fail', 'message': validity_check['message'] } return make_response(jsonify(response_object)), 401 user = validity_check['user'] reuse_check = user.check_reset_token_already_used( reset_password_token) if not reuse_check: response_object = { 'status': 'fail', 'message': 'Token already used' } return make_response(jsonify(response_object)), 401 if not new_password or len(new_password) < 6: response_object = { 'status': 'fail', 'message': 'Password must be at least 6 characters long' } return make_response(jsonify(response_object)), 401 user.hash_password(new_password) user.delete_password_reset_tokens() db.session.commit() response_object = { 'status': 'success', 'message': 'Password changed, please log in' } return make_response(jsonify(response_object)), 200
def phone(self, phone): self._phone = proccess_phone_number(phone)
def post(self): # get the post data post_data = request.get_json() user = None phone = None email = post_data.get('username') or post_data.get('email') password = post_data.get('password') tfa_token = post_data.get('tfa_token') # First try to match email if email: user = User.query.filter_by(email=email).execution_options(show_all=True).first() # Now try to match the public serial number (comes in under the phone) if not user: public_serial_number_or_phone = post_data.get('phone') user = User.query.filter_by(public_serial_number=public_serial_number_or_phone).execution_options( show_all=True).first() # Now try to match the phone if not user: try: phone = proccess_phone_number(post_data.get('phone'), region=post_data.get('region')) except NumberParseException as e: response_object = {'message': 'Invalid Phone Number: ' + str(e)} return make_response(jsonify(response_object)), 401 if phone: user = User.query.filter_by(phone=phone).execution_options(show_all=True).first() # mobile user doesn't exist so default to creating a new wallet! if user is None and phone: # this is a registration from a mobile device THUS a vendor or recipient. response_object, response_code = UserUtils.proccess_create_or_modify_user_request( dict(phone=phone, deviceInfo=post_data.get('deviceInfo')), is_self_sign_up=True, ) if response_code == 200: db.session.commit() return make_response(jsonify(response_object)), response_code if user and user.is_activated and post_data.get('phone') and (password == ''): # user already exists, is activated. no password provided, thus request PIN screen. # todo: this should check if device exists, if no, resend OTP to verify login is real. response_object = { 'status': 'success', 'login_with_pin': True, 'message': 'Login with PIN' } return make_response(jsonify(response_object)), 200 if not (email or post_data.get('phone')): response_object = { 'status': 'fail', 'message': 'No username supplied' } return make_response(jsonify(response_object)), 401 if post_data.get('phone') and user and user.one_time_code and not user.is_activated: # vendor sign up with one time code or OTP verified if user.one_time_code == password: response_object = { 'status': 'success', 'pin_must_be_set': True, 'message': 'Please set your pin.' } return make_response(jsonify(response_object)), 200 if not user.is_phone_verified: if user.is_self_sign_up: # self sign up, resend phone verification code user.set_pin(None, False) # resets PIN UserUtils.send_one_time_code(phone=phone, user=user) db.session.commit() response_object = {'message': 'Please verify phone number.', 'otp_verify': True} return make_response(jsonify(response_object)), 200 try: if not user or not user.verify_password(password): response_object = { 'status': 'fail', 'message': 'Invalid username or password' } return make_response(jsonify(response_object)), 401 if not user.is_activated: response_object = { 'status': 'fail', 'is_activated': False, 'message': 'Account has not been activated. Please check your emails.' } return make_response(jsonify(response_object)), 401 if post_data.get('deviceInfo'): UserUtils.save_device_info(post_data.get('deviceInfo'), user) auth_token = user.encode_auth_token() if not auth_token: response_object = { 'status': 'fail', 'message': 'Invalid username or password' } return make_response(jsonify(response_object)), 401 # Possible Outcomes: # TFA required, but not set up # TFA enabled, and user does not have valid TFA token # TFA enabled, and user has valid TFA token # TFA not required tfa_response_oject = tfa_logic(user, tfa_token) if tfa_response_oject: tfa_response_oject['auth_token'] = auth_token.decode() return make_response(jsonify(tfa_response_oject)), 401 # Update the last_seen TS for this user user.update_last_seen_ts() response_object = create_user_response_object(user, auth_token, 'Successfully logged in.') db.session.commit() return make_response(jsonify(response_object)), 200 except Exception as e: sentry_sdk.capture_exception(e) raise e
def proccess_create_or_modify_user_request(attribute_dict, organisation=None, allow_existing_user_modify=False, is_self_sign_up=False, modify_only=False): """ Takes a create or modify user request and determines the response. Normally what's in the top level API function, but here it's one layer down because there's multiple entry points for 'create user': - The admin api - The register api :param attribute_dict: attributes that can be supplied by the request maker :param organisation: what organisation the request maker belongs to. The created user is bound to the same org :param allow_existing_user_modify: whether to return an error when the user already exists for the supplied IDs :param is_self_sign_up: does the request come from the register api? :param modify_only: whether to allow the creation of a new user :return: An http response """ if not attribute_dict.get('custom_attributes'): attribute_dict['custom_attributes'] = {} user_id = attribute_dict.get('user_id') email = attribute_dict.get('email') phone = attribute_dict.get('phone') account_types = attribute_dict.get('account_types', []) if isinstance(account_types, str): account_types = account_types.split(',') referred_by = attribute_dict.get('referred_by') blockchain_address = attribute_dict.get('blockchain_address') provided_public_serial_number = attribute_dict.get('public_serial_number') uuid = attribute_dict.get('uuid') require_identifier = attribute_dict.get('require_identifier', True) if not user_id: # Extract ID from Combined User ID and Name String if it exists try: user_id_name_string = attribute_dict.get('user_id_name_string') user_id_str = user_id_name_string and user_id_name_string.split( ':')[0] if user_id_str: user_id = int(user_id_str) except SyntaxError: pass if not blockchain_address and provided_public_serial_number: try: blockchain_address = to_checksum_address( provided_public_serial_number) # Since it's actually an ethereum address set the provided public serial number to None # so it doesn't get used as a transfer card provided_public_serial_number = None except Exception: pass require_transfer_card_exists = attribute_dict.get( 'require_transfer_card_exists', g.active_organisation.require_transfer_card) public_serial_number = (provided_public_serial_number or attribute_dict.get('payment_card_qr_code') or attribute_dict.get('payment_card_barcode')) location = attribute_dict.get('location') # address location # Yes, we know "GPS" refers to a technology, but "gps_location" is less ambiguous for end users than "geo_location" gps_location = attribute_dict.get( 'gps_location') # geo location as str of lat, lng use_precreated_pin = attribute_dict.get('use_precreated_pin') use_last_4_digits_of_id_as_initial_pin = attribute_dict.get( 'use_last_4_digits_of_id_as_initial_pin') transfer_account_name = attribute_dict.get('transfer_account_name') first_name = attribute_dict.get('first_name') last_name = attribute_dict.get('last_name') business_usage_name = attribute_dict.get('business_usage_name') business_usage_id = None if business_usage_name: usage = TransferUsage.find_or_create(business_usage_name) business_usage_id = usage.id preferred_language = attribute_dict.get('preferred_language') primary_user_identifier = attribute_dict.get('primary_user_identifier') primary_user_pin = attribute_dict.get('primary_user_pin') initial_disbursement = attribute_dict.get('initial_disbursement', None) if not account_types: account_types = ['beneficiary'] roles_to_set = [] for at in account_types: if at not in g.active_organisation.valid_roles: raise Exception( f'{at} not a valid role for this organisation. Please choose one of the following: {g.active_organisation.valid_roles}' ) roles_to_set.append((ASSIGNABLE_TIERS[at], at)) chain = get_chain() if current_app.config['CHAINS'][chain]['IS_USING_BITCOIN']: try: base58.b58decode_check(blockchain_address) except ValueError: response_object = { 'message': 'Blockchain Address {} Not Valid'.format(blockchain_address) } return response_object, 400 if isinstance(phone, bool): phone = None if phone and not is_self_sign_up: # phone has already been parsed if self sign up try: phone = proccess_phone_number(phone) except NumberParseException as e: response_object = {'message': 'Invalid Phone Number: ' + str(e)} return response_object, 400 # Work out if there's an existing transfer account to bind to existing_transfer_account = None if primary_user_identifier: primary_user, _ = find_user_from_public_identifier( primary_user_identifier) if not primary_user or not primary_user.verify_password( primary_user_pin): response_object = {'message': 'Primary User not Found'} return response_object, 400 if not primary_user.verify_password(primary_user_pin): response_object = {'message': 'Invalid PIN for Primary User'} return response_object, 400 primary_user_transfer_account = primary_user.transfer_account if not primary_user_transfer_account: response_object = { 'message': 'Primary User has no transfer account' } return response_object, 400 if not (phone or email or public_serial_number or blockchain_address or user_id or uuid or not require_identifier): response_object = {'message': 'Must provide a unique identifier'} return response_object, 400 if use_precreated_pin and not public_serial_number: response_object = { 'message': 'Must provide public serial number to use a transfer card or pre-created pin' } return response_object, 400 if public_serial_number: public_serial_number = str(public_serial_number) if use_precreated_pin or require_transfer_card_exists: transfer_card = TransferCard.query.filter_by( public_serial_number=public_serial_number).first() if not transfer_card: response_object = {'message': 'Transfer card not found'} return response_object, 400 business_usage = None if business_usage_id: business_usage = TransferUsage.query.get(business_usage_id) if not business_usage: response_object = { 'message': f'Business Usage not found for id {business_usage_id}' } return response_object, 400 referred_by_user, _ = find_user_from_public_identifier(referred_by) if referred_by and not referred_by_user: response_object = { 'message': f'Referrer user not found for public identifier {referred_by}' } return response_object, 400 existing_user, _ = find_user_from_public_identifier( email, phone, public_serial_number, blockchain_address, uuid) if not existing_user and user_id: existing_user = User.query.get(user_id) if modify_only and existing_user is None: response_object = {'message': 'User not found'} return response_object, 404 if existing_user: if not allow_existing_user_modify: response_object = {'message': 'User already exists for Identifier'} return response_object, 400 try: user = update_transfer_account_user( existing_user, first_name=first_name, last_name=last_name, preferred_language=preferred_language, phone=phone, email=email, public_serial_number=public_serial_number, use_precreated_pin=use_precreated_pin, existing_transfer_account=existing_transfer_account, roles=roles_to_set, business_usage=business_usage) set_location_conditionally(user, location, gps_location) if referred_by_user: user.referred_by.clear( ) # otherwise prior referrals will remain... user.referred_by.append(referred_by_user) set_custom_attributes(attribute_dict, user) flag_modified(user, "custom_attributes") db.session.commit() response_object = { 'message': 'User Updated', 'data': { 'user': user_schema.dump(user).data } } return response_object, 200 except Exception as e: response_object = {'message': str(e)} return response_object, 400 user = create_transfer_account_user( first_name=first_name, last_name=last_name, preferred_language=preferred_language, phone=phone, email=email, public_serial_number=public_serial_number, uuid=uuid, organisation=organisation, blockchain_address=blockchain_address, transfer_account_name=transfer_account_name, use_precreated_pin=use_precreated_pin, use_last_4_digits_of_id_as_initial_pin= use_last_4_digits_of_id_as_initial_pin, existing_transfer_account=existing_transfer_account, roles=roles_to_set, is_self_sign_up=is_self_sign_up, business_usage=business_usage, initial_disbursement=initial_disbursement) set_location_conditionally(user, location, gps_location) if referred_by_user: user.referred_by.append(referred_by_user) if attribute_dict.get('gender'): attribute_dict['custom_attributes']['gender'] = attribute_dict.get( 'gender') if attribute_dict.get('bio'): attribute_dict['custom_attributes']['bio'] = attribute_dict.get('bio') set_custom_attributes(attribute_dict, user) if is_self_sign_up and attribute_dict.get('deviceInfo', None) is not None: save_device_info(device_info=attribute_dict.get('deviceInfo'), user=user) send_onboarding_sms_messages(user) # Location fires an async task that needs to know user ID db.session.flush() if phone: if is_self_sign_up: send_one_time_code(phone=phone, user=user) return { 'message': 'User Created. Please verify phone number.', 'otp_verify': True }, 200 elif current_app.config['ONBOARDING_SMS']: try: send_onboarding_sms_messages(user) except Exception as e: print(e) sentry_sdk.capture_exception(e) pass response_object = { 'message': 'User Created', 'data': { 'user': user_schema.dump(user).data } } return response_object, 200
def post(self): # get the post data post_data = request.get_json() user = None email = post_data.get('username') or post_data.get('email') password = post_data.get('password') tfa_token = post_data.get('tfa_token') # First try to match email if email: user = User.query.filter_by(email=email).first() #Now try to match the public serial number (comes in under the phone) if not user: public_serial_number_or_phone = post_data.get('phone') user = User.query.filter_by( public_serial_number=public_serial_number_or_phone).first() #Now try to match the phone if not user: phone = proccess_phone_number(post_data.get('phone')) if phone: user = User.query.filter_by(phone=phone).first() if not (email or post_data.get('phone')): responseObject = { 'status': 'fail', 'message': 'No username supplied' } return make_response(jsonify(responseObject)), 401 if post_data.get( 'phone' ) and user and user.one_time_code and not user.is_activated: if user.one_time_code == password: responseObject = { 'status': 'success', 'pin_must_be_set': True, 'message': 'Please set your pin.' } return make_response(jsonify(responseObject)), 200 try: if not user or not user.verify_password(post_data.get('password')): responseObject = { 'status': 'fail', 'message': 'Invalid username or password' } return make_response(jsonify(responseObject)), 401 if not user.is_activated: responseObject = { 'status': 'fail', 'is_activated': False, 'message': 'Account has not been activated. Please check your emails.' } return make_response(jsonify(responseObject)), 401 if post_data.get('deviceInfo'): save_device_info(post_data.get('deviceInfo'), user) db.session.commit() auth_token = user.encode_auth_token() if not auth_token: responseObject = { 'status': 'fail', 'message': 'Invalid username or password' } return make_response(jsonify(responseObject)), 401 # Possible Outcomes: # TFA required, but not set up # TFA enabled, and user does not have valid TFA token # TFA enabled, and user has valid TFA token # TFA not required tfa_response_oject = tfa_logic(user, tfa_token) if tfa_response_oject: tfa_response_oject['auth_token'] = auth_token.decode() return make_response(jsonify(tfa_response_oject)), 401 #Update the last_seen TS for this user user.update_last_seen_ts() responseObject = create_user_response_object( user, auth_token, 'Successfully logged in.') return make_response(jsonify(responseObject)), 200 except Exception as e: sentry.captureException() raise e
def proccess_create_or_modify_user_request( attribute_dict, organisation=None, allow_existing_user_modify=False, is_self_sign_up=False, modify_only=False, ): """ Takes a create or modify user request and determines the response. Normally what's in the top level API function, but here it's one layer down because there's multiple entry points for 'create user': - The admin api - The register api :param attribute_dict: attributes that can be supplied by the request maker :param organisation: what organisation the request maker belongs to. The created user is bound to the same org :param allow_existing_user_modify: whether to return and error when the user already exists for the supplied IDs :param is_self_sign_up: does the request come from the register api? :return: An http response """ if not attribute_dict.get('custom_attributes'): attribute_dict['custom_attributes'] = {} user_id = attribute_dict.get('user_id') email = attribute_dict.get('email') phone = attribute_dict.get('phone') referred_by = attribute_dict.get('referred_by') blockchain_address = attribute_dict.get('blockchain_address') provided_public_serial_number = attribute_dict.get('public_serial_number') if not blockchain_address and provided_public_serial_number: try: blockchain_address = to_checksum_address( provided_public_serial_number) # Since it's actually an ethereum address set the provided public serial number to None # so it doesn't get used as a transfer card provided_public_serial_number = None except Exception: pass require_transfer_card_exists = attribute_dict.get( 'require_transfer_card_exists', g.active_organisation.require_transfer_card) public_serial_number = (provided_public_serial_number or attribute_dict.get('payment_card_qr_code') or attribute_dict.get('payment_card_barcode')) location = attribute_dict.get('location') # address location geo_location = attribute_dict.get( 'geo_location') # geo location as str of lat, lng if geo_location: geo = geo_location.split(' ') lat = geo[0] lng = geo[1] else: # TODO: Work out how this passed tests when this wasn't definied properly!?! lat = None lng = None use_precreated_pin = attribute_dict.get('use_precreated_pin') use_last_4_digits_of_id_as_initial_pin = attribute_dict.get( 'use_last_4_digits_of_id_as_initial_pin') transfer_account_name = attribute_dict.get('transfer_account_name') first_name = attribute_dict.get('first_name') last_name = attribute_dict.get('last_name') business_usage_name = attribute_dict.get('business_usage_name') business_usage_id = None if business_usage_name: usage = TransferUsage.find_or_create(business_usage_name) business_usage_id = usage.id preferred_language = attribute_dict.get('preferred_language') primary_user_identifier = attribute_dict.get('primary_user_identifier') primary_user_pin = attribute_dict.get('primary_user_pin') initial_disbursement = attribute_dict.get('initial_disbursement', None) is_vendor = attribute_dict.get('is_vendor', None) if is_vendor is None: is_vendor = attribute_dict.get('vendor', False) is_tokenagent = attribute_dict.get('is_tokenagent', False) is_groupaccount = attribute_dict.get('is_groupaccount', False) # is_beneficiary defaults to the opposite of is_vendor is_beneficiary = attribute_dict.get( 'is_beneficiary', not is_vendor and not is_tokenagent and not is_groupaccount) if current_app.config['IS_USING_BITCOIN']: try: base58.b58decode_check(blockchain_address) except ValueError: response_object = { 'message': 'Blockchain Address {} Not Valid'.format(blockchain_address) } return response_object, 400 if isinstance(phone, bool): phone = None if phone and not is_self_sign_up: # phone has already been parsed if self sign up try: phone = proccess_phone_number(phone) except NumberParseException as e: response_object = {'message': 'Invalid Phone Number: ' + str(e)} return response_object, 400 # Work out if there's an existing transfer account to bind to existing_transfer_account = None if primary_user_identifier: primary_user = find_user_from_public_identifier( primary_user_identifier) if not primary_user or not primary_user.verify_password( primary_user_pin): response_object = {'message': 'Primary User not Found'} return response_object, 400 if not primary_user.verify_password(primary_user_pin): response_object = {'message': 'Invalid PIN for Primary User'} return response_object, 400 primary_user_transfer_account = primary_user.transfer_account if not primary_user_transfer_account: response_object = { 'message': 'Primary User has no transfer account' } return response_object, 400 if not (phone or email or public_serial_number or blockchain_address): response_object = {'message': 'Must provide a unique identifier'} return response_object, 400 if use_precreated_pin and not public_serial_number: response_object = { 'message': 'Must provide public serial number to use a transfer card or pre-created pin' } return response_object, 400 if public_serial_number: public_serial_number = str(public_serial_number) if use_precreated_pin or require_transfer_card_exists: transfer_card = TransferCard.query.filter_by( public_serial_number=public_serial_number).first() if not transfer_card: response_object = {'message': 'Transfer card not found'} return response_object, 400 business_usage = None if business_usage_id: business_usage = TransferUsage.query.get(business_usage_id) if not business_usage: response_object = { 'message': f'Business Usage not found for id {business_usage_id}' } return response_object, 400 referred_by_user = find_user_from_public_identifier(referred_by) if referred_by and not referred_by_user: response_object = { 'message': f'Referrer user not found for public identifier {referred_by}' } return response_object, 400 existing_user = find_user_from_public_identifier(email, phone, public_serial_number, blockchain_address) if modify_only: existing_user = User.query.get(user_id) if modify_only and existing_user is None: response_object = {'message': 'User not found'} return response_object, 404 if existing_user: if not allow_existing_user_modify: response_object = {'message': 'User already exists for Identifier'} return response_object, 400 try: user = update_transfer_account_user( existing_user, first_name=first_name, last_name=last_name, preferred_language=preferred_language, phone=phone, email=email, location=location, public_serial_number=public_serial_number, use_precreated_pin=use_precreated_pin, existing_transfer_account=existing_transfer_account, is_beneficiary=is_beneficiary, is_vendor=is_vendor, is_tokenagent=is_tokenagent, is_groupaccount=is_groupaccount, business_usage=business_usage) if referred_by_user: user.referred_by.clear( ) # otherwise prior referrals will remain... user.referred_by.append(referred_by_user) set_custom_attributes(attribute_dict, user) flag_modified(user, "custom_attributes") db.session.commit() response_object = { 'message': 'User Updated', 'data': { 'user': user_schema.dump(user).data } } return response_object, 200 except Exception as e: response_object = {'message': str(e)} return response_object, 400 user = create_transfer_account_user( first_name=first_name, last_name=last_name, preferred_language=preferred_language, phone=phone, email=email, public_serial_number=public_serial_number, organisation=organisation, blockchain_address=blockchain_address, transfer_account_name=transfer_account_name, lat=lat, lng=lng, use_precreated_pin=use_precreated_pin, use_last_4_digits_of_id_as_initial_pin= use_last_4_digits_of_id_as_initial_pin, existing_transfer_account=existing_transfer_account, is_beneficiary=is_beneficiary, is_vendor=is_vendor, is_tokenagent=is_tokenagent, is_groupaccount=is_groupaccount, is_self_sign_up=is_self_sign_up, business_usage=business_usage, initial_disbursement=initial_disbursement) if referred_by_user: user.referred_by.append(referred_by_user) if attribute_dict.get('gender'): attribute_dict['custom_attributes']['gender'] = attribute_dict.get( 'gender') if attribute_dict.get('bio'): attribute_dict['custom_attributes']['bio'] = attribute_dict.get('bio') set_custom_attributes(attribute_dict, user) if is_self_sign_up and attribute_dict.get('deviceInfo', None) is not None: save_device_info(device_info=attribute_dict.get('deviceInfo'), user=user) send_onboarding_sms_messages(user) # Location fires an async task that needs to know user ID db.session.flush() if location: user.location = location if phone: if is_self_sign_up: send_one_time_code(phone=phone, user=user) return { 'message': 'User Created. Please verify phone number.', 'otp_verify': True }, 200 elif current_app.config['ONBOARDING_SMS']: try: send_onboarding_sms_messages(user) except Exception as e: print(e) sentry_sdk.capture_exception(e) pass response_object = { 'message': 'User Created', 'data': { 'user': user_schema.dump(user).data } } return response_object, 200
def test_create_user(test_client, authed_sempo_admin_user, init_database, create_transfer_account_user, mock_async_set_user_gps_from_location, user_phone_accessor, phone, use_card, business_usage_name, referred_by, gps_location, initial_disbursement, tier, status_code): if tier: authed_sempo_admin_user.set_held_role('ADMIN', tier) auth = get_complete_auth_token(authed_sempo_admin_user) else: auth = None # create the user who is referring create_transfer_account_user.phone = referred_by user_phone_accessor(create_transfer_account_user) payload = { 'first_name': 'John', 'last_name': 'Smith', 'bio': 'EasyMart', 'gender': 'female', 'phone': phone, 'initial_disbursement': initial_disbursement, 'location': 'Elwood', 'business_usage_name': business_usage_name, 'referred_by': user_phone_accessor(create_transfer_account_user) } if gps_location: payload['gps_location'] = gps_location if use_card: public_serial_number = f'{randint(0,999999):06}' new_card = TransferCard(public_serial_number=public_serial_number) init_database.session.add(new_card) init_database.session.commit() payload['public_serial_number'] = public_serial_number response = test_client.post( "/api/v1/user/", headers=dict( Authorization=auth, Accept='application/json' ), json=payload) assert response.status_code == status_code if response.status_code == 200: data = response.json['data'] assert isinstance(data['user'], object) assert data['user']['first_name'] == 'John' assert data['user']['last_name'] == 'Smith' assert data['user']['custom_attributes']['bio'] == 'EasyMart' assert data['user']['custom_attributes']['gender'] == 'female' assert data['user']['phone'] == proccess_phone_number(phone) assert data['user']['is_vendor'] is False assert data['user']['is_tokenagent'] is False assert data['user']['is_groupaccount'] is False assert data['user']['location'] == 'Elwood' assert data['user']['business_usage_id'] == init_database.session.query(TransferUsage)\ .filter_by(name=business_usage_name).first().id assert data['user']['referred_by'] == user_phone_accessor(create_transfer_account_user) if initial_disbursement is not None: assert data['user']['transfer_accounts'][0]['balance'] == initial_disbursement else: db_user = init_database.session.query(User).get(data['user']['id']) assert data['user']['transfer_accounts'][0]['balance'] == db_user.default_organisation.default_disbursement # Checks that we're calling the gps location fetching job, and passing the right data to it fn_inputs = mock_async_set_user_gps_from_location if gps_location: assert data['user']['lat'] == 12.02 assert data['user']['lng'] == -15.04 assert len(fn_inputs) == 0 else: args, kwargs = fn_inputs[-1] assert kwargs == {'user_id': data['user']['id'], 'location': 'Elwood'}
def insert_user(self, ge_user): phone_number = None if 'DELETED' in ge_user['phone'] else ge_user[ 'phone'] if not phone_number: print("Phone Deleted, Skipping") return if ge_user['status'] == 'Deleted': print("User Deleted, Skipping") return processed_phone = proccess_phone_number(phone_number) existing_user = User.query.filter_by( phone=processed_phone).execution_options(show_all=True).first() if existing_user: print(f'User already exists for phone {processed_phone}') return business_usage = None if ge_user.get('business_type') is not None: sempo_category = GE_BUSINESS_CATEGORY_MAPPINGS.get( ge_user['business_type']) if sempo_category: business_usage = TransferUsage.query.filter_by( name=sempo_category).first() organsation = db.session.query(Organisation).get( self.sempo_organisation_id) try: sempo_user = create_transfer_account_user( first_name=ge_user['first_name'], last_name=ge_user['last_name'], organisation=organsation, phone=phone_number, preferred_language=ge_user['preferred_language'], location=ge_user['location'], business_usage=business_usage) sempo_user.pin_hash = ge_user['encrypted_pin'] sempo_user.is_activated = ge_user[ 'status'] == 'Active' # Is this the correct way to find this out? sempo_user.default_transfer_account.is_approved = True sempo_user.is_disabled = False sempo_user.is_phone_verified = True sempo_user.is_self_sign_up = False sempo_user.terms_accepted = False sempo_user.created = ge_user['created_at'] sempo_user.is_market_enabled = int(ge_user['market_enabled']) == 1 sempo_user.custom_attributes = self.create_custom_attributes( ge_user) if ge_user['token_agents.id'] is not None: sempo_user.set_held_role('TOKEN_AGENT', 'grassroots_token_agent') else: # Is this the correct way to find this out or can a benificiary also be a token agent # Or is there some field where you can find this out? sempo_user.set_held_role('BENEFICIARY', 'beneficiary') if ge_user['group_accounts.id'] is not None: sempo_user.set_held_role('GROUP_ACCOUNT', 'grassroots_group_account') db.session.flush() return sempo_user except (IntegrityError, InvalidRequestError) as e: print(e) db.session().rollback()
def post(self): # There is an unique case where users are using their mobile number from the App to either login or register # The app uses g.active_organisation to reference user.transfer_account to send an SMS to the User. # This means that g.active_organisation should default to the master_organisation # For admin users, it doesn't matter as this endpoint is unauthed. g.active_organisation = Organisation.master_organisation() post_data = request.get_json() user = None phone = None email = post_data.get('username', '') or post_data.get('email', '') email = email.lower() if email else '' password = post_data.get('password') # Default pin to password as fallback for old android versions pin = post_data.get('pin', password) tfa_token = post_data.get('tfa_token') password_empty = password == '' or password is None pin_empty = pin == '' or pin is None ratelimit_key = email or post_data.get('phone') if ratelimit_key: limit = rate_limit("login_"+ratelimit_key, 25) if limit: response_object = { 'status': 'fail', 'message': f'Please try again in {limit} minutes' } return make_response(jsonify(response_object)), 403 # First try to match email if email: user = User.query.filter(func.lower(User.email)==email).execution_options(show_all=True).first() # Now try to match the public serial number (comes in under the phone) if not user: public_serial_number_or_phone = post_data.get('phone') user = User.query.filter_by(public_serial_number=public_serial_number_or_phone).execution_options( show_all=True).first() # Now try to match the phone if not user: try: phone = proccess_phone_number(post_data.get('phone'), region=post_data.get('region')) except NumberParseException as e: response_object = {'message': 'Invalid Phone Number: ' + str(e)} return make_response(jsonify(response_object)), 401 if phone: user = User.query.filter_by(phone=phone).execution_options(show_all=True).first() # mobile user doesn't exist so default to creating a new wallet! if user is None and phone and current_app.config['ALLOW_SELF_SIGN_UP']: # this is a registration from a mobile device THUS a vendor or recipient. response_object, response_code = UserUtils.proccess_create_or_modify_user_request( dict(phone=phone, deviceInfo=post_data.get('deviceInfo')), is_self_sign_up=True, ) if response_code == 200: db.session.commit() return make_response(jsonify(response_object)), response_code no_password_or_pin_hash = user and not user.password_hash and not user.pin_hash if post_data.get('phone') and user and user.one_time_code and (not user.is_activated or not user.pin_hash): # vendor sign up with one time code or OTP verified if user.one_time_code == pin: response_object = { 'status': 'success', 'pin_must_be_set': True, 'message': 'Please set your pin.' } return make_response(jsonify(attach_host(response_object))), 200 if not user.is_phone_verified or no_password_or_pin_hash: if user.is_self_sign_up: # self sign up, resend phone verification code user.set_pin(None, False) # resets PIN UserUtils.send_one_time_code(phone=phone, user=user) db.session.commit() if not password_empty: # The user provided a password, so probably not going through incremental login # This is a hacky way to get past the incremental-login multi-org split response_object = { 'status': 'fail', 'otp_verify': True, 'message': 'Please verify phone number.', 'error_message': 'Incorrect One Time Code.' } return make_response(jsonify(attach_host(response_object))), 200 response_object = {'message': 'Please verify phone number.', 'otp_verify': True} return make_response(jsonify(attach_host(response_object))), 200 if user and user.is_activated and post_data.get('phone') and (password_empty and pin_empty): # user already exists, is activated. no password or pin provided, thus request PIN screen. # todo: this should check if device exists, if no, resend OTP to verify login is real. response_object = { 'status': 'success', 'login_with_pin': True, 'message': 'Login with PIN' } return make_response(jsonify(attach_host(response_object))), 200 if not (email or post_data.get('phone')): response_object = { 'status': 'fail', 'message': 'No username supplied' } return make_response(jsonify(response_object)), 401 try: if not (user and (pin and user.verify_pin(pin) or password and user.verify_password(password))): response_object = { 'status': 'fail', 'message': 'Invalid username or password' } return make_response(jsonify(response_object)), 401 if not user.is_activated: response_object = { 'status': 'fail', 'is_activated': False, 'message': 'Account has not been activated. Please check your emails.' } return make_response(jsonify(response_object)), 401 if post_data.get('deviceInfo'): deviceInfo = post_data.get('deviceInfo') UserUtils.save_device_info(deviceInfo, user) auth_token = user.encode_auth_token() if not auth_token: response_object = { 'status': 'fail', 'message': 'Invalid username or password' } return make_response(jsonify(response_object)), 401 # Possible Outcomes: # TFA required, but not set up # TFA enabled, and user does not have valid TFA token # TFA enabled, and user has valid TFA token # TFA not required tfa_response_oject = tfa_logic(user, tfa_token) if tfa_response_oject: tfa_response_oject['auth_token'] = auth_token.decode() return make_response(jsonify(tfa_response_oject)), 401 # Update the last_seen TS for this user user.update_last_seen_ts() response_object = create_user_response_object(user, auth_token, 'Successfully logged in.') db.session.commit() return make_response(jsonify(attach_host(response_object))), 200 except Exception as e: sentry_sdk.capture_exception(e) raise e
def proccess_attribute_dict(attribute_dict, force_dict_keys_lowercase=False, allow_existing_user_modify=False, require_transfer_card_exists=False): elapsed_time('1.0 Start') if force_dict_keys_lowercase: attribute_dict = force_attribute_dict_keys_to_lowercase(attribute_dict) attribute_dict = strip_kobo_preslashes(attribute_dict) attribute_dict = apply_settings(attribute_dict) attribute_dict = truthy_all_dict_values(attribute_dict) attribute_dict = strip_whitespace_characters(attribute_dict) elapsed_time('2.0 Post Processing') email = attribute_dict.get('email') phone = attribute_dict.get('phone') blockchain_address = attribute_dict.get('blockchain_address') provided_public_serial_number = attribute_dict.get('public_serial_number') if not blockchain_address and provided_public_serial_number: try: blockchain_address = utils.checksum_encode( provided_public_serial_number) # Since it's actually an ethereum address set the provided public serial number to None # so it doesn't get used as a transfer card provided_public_serial_number = None except Exception: pass public_serial_number = (provided_public_serial_number or attribute_dict.get('payment_card_qr_code') or attribute_dict.get('payment_card_barcode')) location = attribute_dict.get('location') use_precreated_pin = attribute_dict.get('use_precreated_pin') use_last_4_digits_of_id_as_initial_pin = attribute_dict.get( 'use_last_4_digits_of_id_as_initial_pin') transfer_account_name = attribute_dict.get('transfer_account_name') first_name = attribute_dict.get('first_name') last_name = attribute_dict.get('last_name') primary_user_identifier = attribute_dict.get('primary_user_identifier') primary_user_pin = attribute_dict.get('primary_user_pin') custom_initial_disbursement = attribute_dict.get( 'custom_initial_disbursement', None) is_vendor = attribute_dict.get('is_vendor', None) if is_vendor is None: is_vendor = attribute_dict.get('vendor', False) # is_beneficiary defaults to the opposite of is_vendor is_beneficiary = attribute_dict.get('is_beneficiary', not is_vendor) if current_app.config['IS_USING_BITCOIN']: try: base58.b58decode_check(blockchain_address) except ValueError: response_object = { 'message': 'Blockchain Address {} Not Valid'.format(blockchain_address) } return response_object, 400 if isinstance(phone, bool): phone = None if phone: try: phone = proccess_phone_number(phone) except NumberParseException as e: response_object = {'message': 'Invalid Phone Number: ' + str(e)} return response_object, 400 # Work out if there's an existing transfer account to bind to existing_transfer_account = None if primary_user_identifier: primary_user = find_user_from_public_identifier( primary_user_identifier) if not primary_user or not primary_user.verify_password( primary_user_pin): response_object = {'message': 'Primary User not Found'} return response_object, 400 if not primary_user.verify_password(primary_user_pin): response_object = {'message': 'Invalid PIN for Primary User'} return response_object, 400 primary_user_transfer_account = primary_user.transfer_account if not primary_user_transfer_account: response_object = { 'message': 'Primary User has no transfer account' } return response_object, 400 if not (phone or email or public_serial_number or blockchain_address): response_object = {'message': 'Must provide a unique identifier'} return response_object, 400 if use_precreated_pin and not public_serial_number: response_object = { 'message': 'Must provide public serial number to use a transfer card or pre-created pin' } return response_object, 400 if public_serial_number: public_serial_number = str(public_serial_number) if use_precreated_pin or require_transfer_card_exists: transfer_card = models.TransferCard.query.filter_by( public_serial_number=public_serial_number).first() if not transfer_card: response_object = {'message': 'Transfer card not found'} return response_object, 400 if custom_initial_disbursement and not custom_initial_disbursement <= current_app.config[ 'MAXIMUM_CUSTOM_INITIAL_DISBURSEMENT']: response_object = { 'message': 'Disbursement more than maximum allowed amount ({} {})'.format( current_app.config['MAXIMUM_CUSTOM_INITIAL_DISBURSEMENT'] / 100, current_app.config['CURRENCY_NAME']) } return response_object, 400 existing_user = find_user_from_public_identifier(email, phone, public_serial_number, blockchain_address) if existing_user: if not allow_existing_user_modify: response_object = {'message': 'User already exists for Identifier'} return response_object, 400 user = update_transfer_account_user( existing_user, first_name=first_name, last_name=last_name, phone=phone, email=email, public_serial_number=public_serial_number, use_precreated_pin=use_precreated_pin, existing_transfer_account=existing_transfer_account, is_beneficiary=is_beneficiary, is_vendor=is_vendor) default_attributes, custom_attributes = set_custom_attributes( attribute_dict, user) flag_modified(user, "custom_attributes") db.session.commit() response_object = { 'message': 'User Updated', 'data': { 'user': user_schema.dump(user).data } } return response_object, 200 elapsed_time('3.0 Ready to create') user = create_transfer_account_user( first_name=first_name, last_name=last_name, phone=phone, email=email, public_serial_number=public_serial_number, blockchain_address=blockchain_address, transfer_account_name=transfer_account_name, location=location, use_precreated_pin=use_precreated_pin, use_last_4_digits_of_id_as_initial_pin= use_last_4_digits_of_id_as_initial_pin, existing_transfer_account=existing_transfer_account, is_beneficiary=is_beneficiary, is_vendor=is_vendor) elapsed_time('4.0 Created') default_attributes, custom_attributes = set_custom_attributes( attribute_dict, user) if custom_initial_disbursement: try: disbursement = CreditTransferUtils.make_disbursement_transfer( custom_initial_disbursement, user) except Exception as e: response_object = {'message': str(e)} return response_object, 400 elapsed_time('5.0 Disbursement done') db.session.flush() if location: user.location = location if phone and current_app.config['ONBOARDING_SMS']: try: balance = user.transfer_account.balance if isinstance(balance, int): balance = balance / 100 send_onboarding_message(first_name=user.first_name, to_phone=phone, credits=balance, one_time_code=user.one_time_code) except Exception as e: print(e) pass if user.one_time_code: response_object = { 'message': 'User Created', 'data': { 'user': user_schema.dump(user).data } } else: response_object = { 'message': 'User Created', 'data': { 'user': user_schema.dump(user).data } } elapsed_time('6.0 Complete') return response_object, 200