def ack_auth_token_api(): """endpoint used by clients to ack the auth-token they received""" payload = request.get_json(silent=True) try: user_id, auth_token = extract_headers(request) token = payload.get('token', None) if None in (user_id, token): raise InvalidUsage('bad-request: invalid input') except Exception as e: print(e) raise InvalidUsage('bad-request') if ack_auth_token(user_id, token): increment_metric('auth-token-acked') return jsonify(status='ok') else: return jsonify(status='error', reason='wrong-token'), status.HTTP_400_BAD_REQUEST
def extract_headers(request): """extracts the user_id from the request header""" try: user_id = request.headers.get('X-USERID') auth_token = request.headers.get('X-AUTH-TOKEN', None) except Exception as e: log.error('cant extract user_id from header') raise InvalidUsage('bad header') return user_id, auth_token
def update_user_app_version(user_id, app_ver): """update the user app version""" try: userAppData = UserAppData.query.filter_by(user_id=user_id).first() userAppData.app_ver = app_ver db.session.add(userAppData) db.session.commit() except Exception as e: print(e) raise InvalidUsage('cant set user app data')
def skip_picture_wait(skip_by): try: # stored as string, can be None system_config = db.session.query(SystemConfig).one() system_config.current_picture_index += int(skip_by) db.session.add(system_config) db.session.commit() except Exception as e: print(e) raise InvalidUsage('cant set task result ts')
def blacklist_user_endpoint(): """""" if not config.DEBUG: limit_to_acl() limit_to_password() try: payload = request.get_json(silent=True) user_id = payload.get('user_id', None) if user_id is None: raise InvalidUsage('bad-request') except Exception as e: print(e) raise InvalidUsage('bad-request') else: if blacklist_phone_by_user_id(user_id): return jsonify(status='ok') else: return jsonify(status='error')
def user_phone_number_blacklist_endpoint(): """blacklist a number""" if not config.DEBUG: limit_to_localhost() try: payload = request.get_json(silent=True) phone_number = payload.get('phone-number', None) if phone_number is None: print('user_phone_number_blacklist_endpoint: user_phone: %s' % phone_number) raise InvalidUsage('bad-request') except Exception as e: print(e) raise InvalidUsage('bad-request') if not blacklist_phone_number(phone_number): raise InternalError('cant blacklist number') return jsonify(status='ok')
def add_picture(picture_json, set_active=True): """adds an picture to the db""" import uuid, json from tippicserver.utils import test_url print(picture_json) picture = Picture() try: picture.picture_id = str(uuid.uuid4()) picture.title = picture_json['title'] picture.image_url = picture_json['image_url'] picture.author = { 'user_id': picture_json['user_id'], 'public_address': get_address_by_userid(picture_json['user_id']) } picture.picture_order_index = get_current_order_index() + 1 if get_user(picture_json['user_id']) is None: raise InvalidUsage('no such user_id') if 'skip_image_test' not in picture_json and not test_url(picture_json['image_url']): raise InvalidUsage('image url invalid') db.session.add(picture) db.session.commit() user = get_user(picture_json['user_id']) if not user.username: if not set_username(picture_json['user_id'], picture_json['username']): print("username %s already taken" % picture_json['username']) else: print("user already has a username: %s. Will not set the username to %s " % (user.username, picture_json['username'])) except Exception as e: print(e) print('cant add picture to db with picture_id %s' % picture.picture_id) return e else: if set_active: set_picture_active(picture.picture_id, True) return True
def get_block_clients_below_version(os_type): sysconfig = get_system_config() if not sysconfig: #log.error('cant find value for block-clients-below in the db. using default') return '0' if os_type == OS_ANDROID: return sysconfig.block_clients_below_version_android elif os_type == OS_IOS: return sysconfig.block_clients_below_version_ios raise InvalidUsage('no such os_type: %s' % os_type)
def email_backup_endpoint(): """generates an email with the user's backup details and sends it""" user_id, auth_token = extract_headers(request) if config.AUTH_TOKEN_ENFORCED and not validate_auth_token( user_id, auth_token): print( 'received a bad auth token from user_id %s: %s. ignoring for now' % (user_id, auth_token)) if config.AUTH_TOKEN_ENFORCED and not is_user_authenticated(user_id): abort(403) try: payload = request.get_json(silent=True) to_address = payload.get('to_address', None) enc_key = payload.get('enc_key', None) if None in (to_address, enc_key): raise InvalidUsage('bad-request') # TODO validate email address is legit except Exception as e: print(e) raise InvalidUsage('bad-request') #get_template from db, generate email and send with ses from .models.email_template import EMAIL_TEMPLATE_BACKUP_NAG_1 template_dict = get_email_template_by_type(EMAIL_TEMPLATE_BACKUP_NAG_1) if not template_dict: raise InternalError('cant fetch email template') from .send_email import send_mail_with_qr_attachment try: res = send_mail_with_qr_attachment(template_dict['sent_from'], [to_address], template_dict['title'], template_dict['body'], enc_key) print('email result: %s' % res) increment_metric('backup-email-sent-success') except Exception as e: log.error('failed to sent backup email to %s. e:%s' % (to_address, e)) increment_metric('backup-email-sent-failure') #TODO handle errors return jsonify(status='ok')
def update_available_below_version(os_type): sysconfig = get_system_config() if not sysconfig: log.warning( 'cant find value for update-available-below in the db. using default' ) return '0' if os_type == OS_ANDROID: return sysconfig.update_available_below_version_android elif os_type == OS_IOS: return sysconfig.update_available_below_version_ios raise InvalidUsage('no such os_type: %s' % os_type)
def add_pictures_endpoint(): """used to add pictures to the db""" if not config.DEBUG: limit_to_acl() limit_to_password() payload = request.get_json(silent=True) try: pictures = payload.get('pictures', None) except Exception as e: print('exception: %s' % e) raise InvalidUsage('bad-request') for picture in pictures: status = add_picture(picture) if status is not True: raise InvalidUsage(message='error', payload={ 'error field': str(status), 'picture': picture }) return jsonify(status='ok')
def nuke_user_api(): """internal endpoint used to nuke a user's task and tx data. use with care""" if not config.DEBUG: limit_to_acl() limit_to_password() try: payload = request.get_json(silent=True) phone_number = payload.get('phone_number', None) nuke_all = payload.get('nuke_all', False) == True if None in (phone_number, ): raise InvalidUsage('bad-request') except Exception as e: print(e) raise InvalidUsage('bad-request') user_ids = nuke_user_data(phone_number, nuke_all) if user_ids is None: print('could not find any user with this number: %s' % phone_number) return jsonify(status='error', reason='no_user') else: print('nuked users with phone number: %s and user_ids %s' % (phone_number, user_ids)) return jsonify(status='ok', user_id=user_ids)
def set_picture_active(picture_id, is_active): """enable/disable picture by offer_id""" picture = Picture.query.filter_by(picture_id=picture_id).first() if not picture: raise InvalidUsage('no such picture_id') picture.is_active = is_active try: db.session.add(picture) db.session.commit() except Exception as e: print(e) print('cant set_picture_active with picture_id %s' % picture_id) return False return True
def get_pictures_summery_endpoint(): """ return a list of shown pictures and tips sum for each""" user_id, auth_token = extract_headers(request) if user_id is None: raise InvalidUsage('invalid payload') print('getting picture-summery for userid %s' % user_id) # dont serve users with no phone number if config.PHONE_VERIFICATION_REQUIRED and not is_user_phone_verified( user_id): print('blocking user %s from getting tasks: phone not verified' % user_id) return jsonify(status='denied'), status.HTTP_403_FORBIDDEN return jsonify(summery=get_pictures_summery(user_id))
def get_block_user_endpoint(): """ return user's block list """ user_id, auth_token = extract_headers(request) if user_id is None: raise InvalidUsage('invalid payload') print('getting block for userid %s' % user_id) # dont serve users with no phone number if config.PHONE_VERIFICATION_REQUIRED and not is_user_phone_verified( user_id): print('blocking user %s from getting tasks: phone not verified' % user_id) return jsonify(status='denied'), status.HTTP_403_FORBIDDEN return jsonify(get_user_blocked_users(user_id))
def skip_picture_endpoint(): """advances current_picture_index""" if not config.DEBUG: limit_to_localhost() try: payload = request.get_json(silent=True) skip_by = payload.get('skip_by', 1) except Exception as e: print(e) raise InvalidUsage('bad-request') else: skip_picture_wait(skip_by) increment_metric('skip-picture-wait') return jsonify(status='ok')
def unblock_user(user_id, user_id_to_unblock): """ add user_id_to_block to user's blocked list """ try: user_app_data = get_user_app_data(user_id) # check if user-to-be-block is valid user_to_block = get_user(user_id_to_unblock) if None in (user_to_block, user_app_data): raise InvalidUsage('no such user_id') except Exception: return False else: user_app_data.blocked_users.remove(user_id_to_unblock) flag_modified(user_app_data, "blocked_users") db.session.add(user_app_data) db.session.commit() return True
def set_should_solve_captcha(user_id, value=0): """sets the should solve captcha. note that client version isn't checked here normally, you would set this value to zero, to show captcha on the next (not current) task. setting it to 1 might cause a (temporary) client error, when the client attempts to submit a (stale) task w/o a captcha, although one is required. it'll be fine on the next attempt. """ try: userAppData = UserAppData.query.filter_by(user_id=user_id).first() userAppData.should_solve_captcha_ternary = value db.session.add(userAppData) db.session.commit() except Exception as e: print(e) raise InvalidUsage('cant set user should_solve_captcha_ternary') else: return True
def app_launch(): """called whenever the app is launched updates the user's last-login time, also forwards some config items to the client """ payload = request.get_json(silent=True) app_ver, user_id = None, None try: user_id, auth_token = extract_headers(request) app_ver = payload.get('app_ver', None) except Exception as e: raise InvalidUsage('bad-request') update_ip_address(user_id, get_source_ip(request)) update_user_app_version(user_id, app_ver) return jsonify(status='ok', config=get_user_config(user_id))
def extract_tx_payment_data(tx_hash): """ensures that the given tx_hash is a valid payment tx, and return a dict with the memo, amount and to_address""" if tx_hash is None: raise InvalidUsage('invalid params') # get the tx_hash data. this might take a second, # so retry while 'Resource Missing' is recevied count = 0 tx_data = None while count < config.STELLAR_TIMEOUT_SEC: try: tx_data = app.kin_sdk.get_transaction_data(tx_hash) except kin.KinErrors.ResourceNotFoundError: count = count + 1 sleep(1) else: break if tx_data is None: print('could not get tx_data for tx_hash: %s. waited %s seconds' % (tx_hash, count)) increment_metric('tx_data_timeout') return False, {} # get the simple op: op = tx_data.operation # verify op type from kin.transactions import OperationTypes if op.type != OperationTypes.PAYMENT: print('unexpected type: %s' % op.type) return False, {} # assemble the result dict data = { 'memo': tx_data.memo, 'amount': op.amount, 'to_address': op.destination } return True, data
def update_ip_address(user_id, ip_address): try: userAppData = UserAppData.query.filter_by(user_id=user_id).first() if userAppData.ip_address == ip_address: # nothing to update return userAppData.ip_address = ip_address try: userAppData.country_iso_code = app.geoip_reader.get( ip_address)['country']['iso_code'] except Exception as e: log.error('could not calc country iso code for %s' % ip_address) db.session.add(userAppData) db.session.commit() except Exception as e: log.error('update_ip_address: e: %s' % e) raise InvalidUsage('cant set user ip address') else: log.info('updated user %s ip to %s' % (user_id, ip_address))
def get_next_picture(): """returns current picture for user""" user_id, auth_token = extract_headers(request) if user_id is None: raise InvalidUsage('invalid payload') print('getting picture for userid %s' % user_id) # dont serve users with no phone number if config.PHONE_VERIFICATION_REQUIRED and not is_user_phone_verified( user_id): print('blocking user %s from getting tasks: phone not verified' % user_id) return jsonify(reason='denied'), status.HTTP_403_FORBIDDEN picture = get_picture_for_user(user_id) print('picture returned for user %s: %s' % (user_id, picture)) if picture.get('blocked', False): return jsonify(error="blocked_user") return jsonify(picture=picture)
def set_user_phone_number(user_id, number): """sets a phone number to the user's entry""" try: user = get_user(user_id) encrypted_number = app.encryption.encrypt(number) # allow (and ignore) re-submissions of the SAME number, but reject new numbers if user.enc_phone_number is not None: if user.enc_phone_number == encrypted_number: return # all good, do nothing else: log.error('refusing to overwrite phone number for user_id %s' % user_id) raise InvalidUsage( 'trying to overwrite an existing phone number with a different one' ) else: user.enc_phone_number = encrypted_number db.session.add(user) db.session.commit() except Exception as e: log.error('cant add phone number %s to user_id: %s. Exception: %s' % (number, user_id, e)) raise
def get_user_backup_hints_by_enc_phone(enc_phone_number): """return the user backup hints object for the given enc_phone_number or throws exception""" ubh = PhoneBackupHints.query.filter_by(enc_phone_number=enc_phone_number).first() if not ubh: raise InvalidUsage('no such enc_phone_number') return ubh
def get_user(user_id): user = User.query.filter_by(user_id=user_id).first() if not user: raise InvalidUsage('no such user_id') return user
def onboard_user(): """creates a wallet for the user and deposits some xlms there""" # input sanity try: user_id, auth_token = extract_headers(request) public_address = request.get_json(silent=True).get( 'public_address', None) if None in (public_address, user_id): raise InvalidUsage('bad-request') except Exception as e: raise InvalidUsage('bad-request') # block users with an older version from onboarding. and send them a push message if should_block_user_by_client_version(user_id): print( 'blocking + deactivating user %s on onboarding with older version' % user_id) # send_please_upgrade_push_2([user_id]) # and also, deactivate the user deactivate_user(user_id) abort(403) elif config.PHONE_VERIFICATION_REQUIRED and not is_user_phone_verified( user_id): raise InvalidUsage('user isnt phone verified') onboarded = is_onboarded(user_id) if onboarded is True: raise InvalidUsage('user already has an account and has been awarded') elif onboarded is None: raise InvalidUsage('no such user exists') else: # create an account, provided none is already being created lock = redis_lock.Lock(app.redis, 'address:%s' % public_address) if lock.acquire(blocking=False): try: if not active_account_exists(public_address): print('creating account with address %s and amount %s' % (public_address, config.STELLAR_INITIAL_ACCOUNT_BALANCE)) tx_id = create_account( public_address, config.STELLAR_INITIAL_ACCOUNT_BALANCE) if not tx_id: raise InternalError('failed to create account at %s' % public_address) elif not award_user(user_id, public_address): raise InternalError( 'unable to award user with %d Kin' % get_initial_reward()) elif not award_user(user_id, public_address): raise InternalError('unable to award user with %d Kin' % get_initial_reward()) except Exception as e: print('exception trying to create account:%s' % e) raise InternalError('unable to create account') finally: lock.release() else: raise InvalidUsage( 'already creating account for user_id: %s and address: %s' % (user_id, public_address)) increment_metric('user_onboarded') return jsonify(status='ok')
def get_user_app_data(user_id): user_app_data = UserAppData.query.filter_by(user_id=user_id).first() if not user_app_data: raise InvalidUsage('no such user_id') return user_app_data
def set_user_phone_number_endpoint(): """get the firebase id token and extract the phone number from it""" payload = request.get_json(silent=True) try: user_id, auth_token = extract_headers(request) token = payload.get('token', None) unverified_phone_number = payload.get('phone_number', None) # only used in tests if None in (user_id, token): raise InvalidUsage('bad-request') except Exception as e: print(e) raise InvalidUsage('bad-request') if not config.DEBUG: print('extracting verified phone number fom firebase id token...') verified_number = extract_phone_number_from_firebase_id_token(token) if verified_number is None: print('bad id-token: %s' % token) return jsonify(status='error', reason='bad_token'), status.HTTP_404_NOT_FOUND # reject blacklisted phone prefixes for prefix in app.blocked_phone_prefixes: if verified_number.find(prefix) == 0: os_type = get_user_os_type(user_id) print( 'found blocked phone prefix (%s) in verified phone number (%s), userid (%s), OS (%s): aborting' % (prefix, verified_number, user_id, os_type)) abort(403) phone = verified_number else: #DEBUG # for tests, you can use the unverified number if no token was given if token: phone = extract_phone_number_from_firebase_id_token(token) if not phone: print('using un-verified phone number in debug') phone = unverified_phone_number.strip().replace('-', '') if not phone: print('could not extract phone in debug') return jsonify(status='error', reason='no_phone_number') # limit the number of registrations a single phone number can do, unless they come from the ACL if not limit_to_acl( return_bool=True) and count_registrations_for_phone_number( phone) > int(config.MAX_NUM_REGISTRATIONS_PER_NUMBER) - 1: print( 'rejecting registration from user_id %s and phone number %s - too many re-registrations' % (user_id, phone)) increment_metric("reject-too-many_registrations") abort(403) print('updating phone number for user %s' % user_id) set_user_phone_number(user_id, phone) increment_metric('user-phone-verified') # return success and the backup hint, if they exist hints = get_backup_hints(user_id) if config.DEBUG: print('restore hints for user_id, phone: %s: %s: %s' % (user_id, phone, hints)) return jsonify(status='ok', hints=hints)
def register_api(): """ register a user to the system called once by every client until 200OK is received from the server. the payload may contain a optional push token. this function may be called by the client multiple times to update fields """ payload = request.get_json(silent=True) try: # add redis lock here? user_id = payload.get('user_id', None) os = payload.get('os', None) device_model = payload.get('device_model', None) time_zone = payload.get('time_zone', None) device_id = payload.get('device_id', None) app_ver = payload.get('app_ver', None) # optionals token = payload.get('token', None) package_id = payload.get('package_id', None) if None in ( user_id, os, device_model, time_zone, app_ver ): # token is optional, device-id is required but may be None raise InvalidUsage('bad-request') if os not in (utils.OS_ANDROID, utils.OS_IOS): raise InvalidUsage('bad-request') if 'Genymotion'.upper() in device_model.upper( ): # block android emulator print('refusing to register Genymotion devices. user_id %s' % user_id) raise InvalidUsage('bad-request') user_id = UUID(user_id) # throws exception on invalid uuid except Exception as e: raise InvalidUsage('bad-request') else: try: new_user_created = create_user(user_id, os, device_model, token, time_zone, device_id, app_ver, package_id) except InvalidUsage as e: raise InvalidUsage('duplicate-userid') else: if new_user_created: print('created user with user_id %s' % user_id) increment_metric('user_registered') else: print('updated userid %s data' % user_id) #TODO find a way to dry up this code which is redundant with get_user_config() # turn off phone verfication for older clients: disable_phone_verification = False disable_backup_nag = False if os == OS_ANDROID and LooseVersion(app_ver) <= LooseVersion( config.BLOCK_ONBOARDING_ANDROID_VERSION): disable_phone_verification = True disable_backup_nag = True elif os == OS_IOS and LooseVersion(app_ver) <= LooseVersion( config.BLOCK_ONBOARDING_IOS_VERSION): disable_phone_verification = True disable_backup_nag = True global_config = get_global_config() if disable_phone_verification: print( 'disabling phone verification for registering userid %s' % user_id) global_config['phone_verification_enabled'] = False if disable_backup_nag: print('disabling backup nag for registering userid %s' % user_id) global_config['backup_nag'] = False # if should_force_update(os, app_ver): # global_config['force_update'] = True # if is_update_available(os, app_ver): # global_config['is_update_available'] = True # return global config - the user doesn't have user-specific config (yet) return jsonify(status='ok', config=global_config)