def test_create_transfer_account_user( test_client, init_database, create_master_organisation, last_name, location, lat, lng, initial_disbursement): from flask import g g.active_organisation = create_master_organisation assert proccess_create_or_modify_user_request( attribute_dict=dict( first_name='Lilly', last_name=last_name, phone=fake.msisdn(), location=location, lat=lat, lng=lng, initial_disbursement=initial_disbursement))
def put(self, user_id): put_data = request.get_json() put_data['user_id'] = user_id response_object, response_code = UserUtils.proccess_create_or_modify_user_request( put_data, organisation=g.active_organisation, allow_existing_user_modify=True, modify_only=True) if response_code == 200: db.session.commit() return make_response(jsonify(response_object)), response_code
def post(self, user_id): post_data = request.get_json() organisation = g.get('active_organisation') if organisation is None: return make_response( jsonify({'message': 'Organisation must be set'})), 400 response_object, response_code = UserUtils.proccess_create_or_modify_user_request( post_data, organisation=organisation) if response_code == 200: db.session.commit() return make_response(jsonify(response_object)), response_code
def post(self, user_id): """ :arg preprocess: whether the data should be cleaned before attempting to create a user - for example converting keys such as "Phone" to "phone". Useful for third-party webhooks where we don't necessarily have a lot of control over how the user will specify fields. :arg allow_as_update: Whether to return an error when the user already exists for the supplied IDs, or instead update the existing user as a PUT request would. Useful for third-party webhooks where PUT requests aren't supported. :arg: return_raw_on_error: whether to return the raw input data on an error - useful for diagnosing what a webhook actually tried to submit """ raw_data = request.get_json() preprocess = request.args.get('preprocess', '').lower() == 'true' #Defaults to false allow_as_update = request.args.get( 'allow_as_update', '').lower() == 'true' #Defaults to false return_raw_on_error = request.args.get( 'return_raw_on_error', '').lower() == 'true' #Defaults to false organisation = g.get('active_organisation') if organisation is None: return make_response( jsonify({'message': 'Organisation must be set'})), 400 if preprocess: data = standard_user_preprocess(raw_data, CREATE_USER_SETTINGS) else: data = raw_data response_object, response_code = UserUtils.proccess_create_or_modify_user_request( data, organisation=organisation, allow_existing_user_modify=allow_as_update) if response_code == 200: db.session.commit() elif return_raw_on_error: response_object['raw_data'] = raw_data return make_response(jsonify(response_object)), response_code
def post(self): # get the post data post_data = request.get_json() is_vendor = post_data.get('isVendor', False) header_postions = post_data.get('headerPositions') self.diagnostics = [] for datarow in post_data.get('data'): attribute_dict = {} contains_anything = False for key, header_label in header_postions.items(): attribute = datarow.get(key) if attribute: contains_anything = True attribute_dict[header_label] = attribute if contains_anything: item_response_object, response_code = UserUtils.proccess_create_or_modify_user_request(attribute_dict, force_dict_keys_lowercase=True, allow_existing_user_modify=True) self.diagnostics.append((item_response_object.get('message'), response_code)) if response_code == 200: db.session.commit() else: db.session.flush() response_object = { 'status': 'success', 'message': 'Successfully Saved.', 'diagnostics': self.diagnostics } return make_response(jsonify(response_object)), 201
def post(self, user_id): post_data = request.get_json() # Data supplied to the API via integrations such as KoboToolbox can be messy, so clean the data first dict_processor = AttributeDictProccessor(post_data) dict_processor.force_attribute_dict_keys_to_lowercase() dict_processor.strip_kobo_preslashes() dict_processor.attempt_to_truthy_dict_values() dict_processor.strip_weirdspace_characters() dict_processor.insert_settings_from_databse(CREATE_USER_SETTINGS) post_data = dict_processor.attribute_dict post_data = UserUtils.extract_kobo_custom_attributes(post_data) # TODO: Kobo should have a more intelligent way of specifying the organisation response_object, response_code = UserUtils.proccess_create_or_modify_user_request( post_data, organisation=g.user.fallback_active_organisation()) if response_code == 200: db.session.commit() return make_response(jsonify(response_object)), response_code
def post(self): # get the post data post_data = request.get_json() email = post_data.get('email') or post_data.get('username') password = post_data.get('password') phone = post_data.get('phone') referral_code = post_data.get('referral_code') if phone is not None: # this is a registration from a mobile device THUS a vendor or recipient. response_object, response_code = UserUtils.proccess_create_or_modify_user_request( post_data, is_self_sign_up=True, ) if response_code == 200: db.session.commit() return make_response(jsonify(response_object)), response_code email_ok = False whitelisted_emails = EmailWhitelist.query\ .filter_by(referral_code=referral_code, used=False) \ .execution_options(show_all=True).all() selected_whitelist_item = None exact_match = False tier = None sempoadmin_emails = current_app.config['SEMPOADMIN_EMAILS'] if sempoadmin_emails != [''] and email in sempoadmin_emails: email_ok = True tier = 'sempoadmin' for whitelisted in whitelisted_emails: if whitelisted.allow_partial_match and whitelisted.email in email: email_ok = True tier = whitelisted.tier selected_whitelist_item = whitelisted exact_match = False continue elif whitelisted.email == email: email_ok = True whitelisted.used = True tier = whitelisted.tier selected_whitelist_item = whitelisted exact_match = True continue if not email_ok: response_object = { 'status': 'fail', 'message': 'Invalid email domain.', } return make_response(jsonify(response_object)), 403 if len(password) < 7: response_object = { 'status': 'fail', 'message': 'Password must be at least 6 characters long', } return make_response(jsonify(response_object)), 403 # check if user already exists user = User.query.filter_by(email=email).execution_options(show_all=True).first() if user: response_object = { 'status': 'fail', 'message': 'User already exists. Please Log in.', } return make_response(jsonify(response_object)), 403 if tier is None: tier = 'subadmin' if selected_whitelist_item: organisation = selected_whitelist_item.organisation else: organisation = Organisation.master_organisation() user = User(blockchain_address=organisation.primary_blockchain_address) user.create_admin_auth(email, password, tier, organisation) # insert the user db.session.add(user) db.session.flush() if exact_match: user.is_activated = True auth_token = user.encode_auth_token() # Possible Outcomes: # TFA required, but not set up # TFA not required tfa_response_oject = tfa_logic(user, tfa_token=None) if tfa_response_oject: tfa_response_oject['auth_token'] = auth_token.decode() db.session.commit() # need this here to commit a created user to the db 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 activated.') db.session.commit() return make_response(jsonify(response_object)), 201 activation_token = user.encode_single_use_JWS('A') send_activation_email(activation_token, email) db.session.commit() # generate the auth token response_object = { 'status': 'success', 'message': 'Successfully registered. You must activate your email.', } return make_response(jsonify(response_object)), 201
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 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