def route_authentication_logout(api_version, account_type): if api_version == "v1": params_dict = routing.get_params_dict(request) if account_type == "helper": authentication_result = tokening.check_helper_api_key(params_dict) if authentication_result["status"] != "ok": return formatting.postprocess_response(authentication_result) # Route will always return {"status": "ok"} return helper_logout(params_dict['email'], params_dict['api_key']), 200 elif account_type == "admin": authentication_result = tokening.check_admin_api_key(params_dict) if tokening.check_admin_api_key(params_dict)["status"] != "ok": return formatting.postprocess_response(authentication_result) # Route will always return {"status": "ok"} return admin_logout(params_dict['email'], params_dict['api_key']), 200 else: return formatting.status("account_type invalid"), 400 else: # Programming Error return formatting.status("api_version invalid"), 400
def enqueue(call_id): call = calls_collection.find_one({'_id': ObjectId(call_id)}) if call is None: return formatting.status('call_id invalid') if call_queue.find_one({'call_id': ObjectId(call_id)}, { '_id': 0, 'call_id': 1 }) is not None: return formatting.status('call already in queue') if ('language' not in call) or ('call_type' not in call) or \ ('zip_code' not in call) or ('timestamp_received' not in call): return formatting.status('call record invalid') new_call = { 'call_id': ObjectId(call_id), 'call_type': call['call_type'], 'zip_code': call['zip_code'], 'language': call['language'], 'timestamp_received': call['timestamp_received'], } call_queue.insert_one(new_call) return formatting.status('ok')
def route_database_performance(api_version, zip_code): if api_version == "v1": performance_dict = performance_scripts.get_performance(zip_code) return formatting.status( "ok", performance=performance_dict["performance"]), 200 else: return formatting.status("api_version invalid"), 400
def admin_login_password(email, password): admin_account = admin_accounts_collection.find_one({'email': email}) if admin_account is not None: if tokening.check_password(password, admin_account['hashed_password']): api_key = admin_create_new_api_key(email) return formatting.status('ok', email=email, api_key=api_key) return formatting.status('email/password invalid')
def get_forward(email): helper_account = helper_accounts_collection.find_one({'email': email}) if helper_account is None: return formatting.status( "server error - missing helper record after successful authentication" ) return formatting.status("ok", forward=helper_account['forward'])
def admin_login_api_key(email, api_key, new_api_key=False): admin_api_key = admin_api_keys_collection.find_one({'email': email}) if admin_api_key is not None: if api_key == admin_api_key['api_key']: if new_api_key: api_key = admin_create_new_api_key(email) return formatting.status('ok', email=email, api_key=api_key) return formatting.status('email/api_key invalid')
def helper_login_api_key(email, api_key, new_api_key=False): helper_api_key = helper_api_keys_collection.find_one({'email': email}) if helper_api_key is not None: if api_key == helper_api_key['api_key']: if new_api_key: api_key = helper_create_new_api_key(email) return formatting.status("ok", email=email, api_key=api_key) return formatting.status('email/api_key invalid')
def helper_login_password(email, password): helper_account = helper_accounts_collection.find_one({'email': email}) if helper_account is not None: if tokening.check_password( password, helper_account['account']['hashed_password']): api_key = helper_create_new_api_key(email) return formatting.status("ok", email=email, api_key=api_key) return formatting.status('email/password invalid')
def fetch(email): record = phone_tokens_collection.find_one({'email': email}, { '_id': 0, 'phone_number': 1 }) if record is None: return formatting.status('not verification found'), 400 if record['phone_number'] == '': return formatting.status('not verification found'), 400 return formatting.status('ok', phone_number=record['phone_number']), 200
def route_docs(api_version): if api_version == "v1": return redirect("https://app.swaggerhub.com/apis-docs/dostuffthatmatters/helperline-backend/1.0"), 302 else: return formatting.status("api_version invalid"), 400
def modify_account(params_dict): existing_document = helper_accounts_collection.find_one( {'email': params_dict["email"]}) existing_account = existing_document["account"] new_account = params_dict["account"] update_dict = {} if 'new_email' in new_account: if (existing_document["email"] != new_account["new_email"]): if (existing_account['email_verified']): return formatting.status('email already verified') else: update_dict.update({"email": new_account["new_email"]}) if 'old_password' in new_account and 'new_password' in new_account: if tokening.check_password(new_account['old_password'], existing_account['hashed_password']): update_dict.update({ "account.hashed_password": tokening.hash_password(new_account['new_password']) }) else: return formatting.status('old_password invalid') if 'zip_code' in new_account: update_dict.update({"account.zip_code": new_account['zip_code']}) if 'country' in new_account: update_dict.update({"account.country": new_account['country']}) if len(update_dict) != 0: helper_accounts_collection.update_one( {'email': existing_document["email"]}, {'$set': update_dict}) if "email" in update_dict: # Send new verification email if new email valid email_tokens_collection.delete_many( {'email': existing_document["email"]}) helper_api_keys_collection.update_one( {'email': existing_document["email"]}, {'$set': { 'email': update_dict["email"] }}) email_verification.trigger(update_dict["email"]) return formatting.status("ok")
def get_account(email): helper_account = helper_accounts_collection.find_one( {'email': email}, {'account.hashed_password': 0}) if helper_account is None: return formatting.server_error_helper_record return formatting.status("ok", account=helper_account['account'])
def verify(token, phone_number): record = phone_tokens_collection.find_one({ 'token': token, 'timestamp_issued': { '$gt': timing.get_current_time(offset_minutes=-3) }, }) if record is None: return formatting.status('token invalid'), 400 phone_tokens_collection.update_one( {'token': token}, {'$set': { 'phone_number': phone_number }}) return formatting.status('ok'), 200
def route_authentication_login(api_version, account_type): if api_version == "v1": params_dict = routing.get_params_dict(request) # Artificial delay to further prevent brute forcing time.sleep(0.05) email = params_dict['email'] password = params_dict['password'] api_key = params_dict['api_key'] if account_type == "helper": # Initial login if email is not None and password is not None: login_result = helper_login_password(email, password) return formatting.postprocess_response(login_result) # Automatic re-login from webapp elif email is not None and api_key is not None: login_result = helper_login_api_key(email, api_key) return formatting.postprocess_response(login_result) elif account_type == "admin": # initial login if email is not None and password is not None: login_result = admin_login_password(email, password) return formatting.postprocess_response(login_result) # automatic re-login from webapp elif email is not None and api_key is not None: login_result = admin_login_api_key(email, api_key) return formatting.postprocess_response(login_result) else: return formatting.status("account_type invalid"), 400 return formatting.status('email/password/api_key missing'), 400 else: # Programming Error return formatting.status("api_version invalid"), 400
def check_admin_api_key(params_dict, new_api_key=False): email = params_dict['email'] api_key = params_dict['api_key'] if email is not None and api_key is not None: return admin_authentication.admin_login_api_key( email, api_key, new_api_key=new_api_key) else: return formatting.status('email/api_key missing')
def send(email, verification_token): verification_url = f"{BACKEND_URL}v1/verification/email/verify/{verification_token}" message = Mail( from_email='*****@*****.**', to_emails=email, subject='Verify your account!', html_content=f'<h2>Welcome to HelperLine!</h2>' + f'<p>Please verify this email address: <a href=\'{verification_url}\'>Verification Link</a></p>' + f'<p>If you have not signed up for our service, you can just ignore this email</p>' + f'<p>Best,<br/>The HelperLine Team</p>') try: sg = SendGridAPIClient(SENDGRID_API_KEY) response = sg.send(message) return formatting.status('ok') except Exception as e: print(e) return formatting.status('email sending failed')
def modify_filter(params_dict): # params_dict["filter"] has already been validated helper_accounts_collection.update_one( {'email': params_dict["email"]}, {'$set': { 'filter': params_dict["filter"] }}) return formatting.status("ok")
def create_account(params_dict): email = params_dict["account"]["email"] password = params_dict["account"]["password"] zip_code = params_dict["account"]["zip_code"] country = params_dict["account"]["country"] current_timestamp = timing.get_current_time() new_helper = { 'email': email, 'account': { 'register_date': timing.datetime_to_string(current_timestamp), 'email_verified': False, 'phone_number': '', 'phone_number_verified': False, 'hashed_password': tokening.hash_password(password), 'zip_code': zip_code, 'country': country, }, 'filter': { 'call_type': { 'only_local': False, 'only_global': False, }, 'language': { 'german': False, 'english': False, }, }, 'forward': { 'online': False, 'stay_online_after_call': False, 'schedule_active': False, 'schedule': [], 'last_modified': current_timestamp } } try: # inserting helper document helper_id = helper_accounts_collection.insert_one( new_helper).inserted_id except DuplicateKeyError as e: # If two people sign up exactla at once # (verfication done but inserting fails for one) print(f'DuplicateKeyError: {e}') return formatting.status('email already taken') # Send verification email and add verification record email_verification.trigger(email) # login and return email/api_key dict return helper_authentication.helper_login_password(email, password)
def add_caller(phone_number): existing_caller = caller_accounts_collection.find_one( {'phone_number': phone_number}) if existing_caller is None: new_caller = {'phone_number': phone_number, 'calls': []} caller_id = caller_accounts_collection.insert_one( new_caller).inserted_id else: caller_id = existing_caller['_id'] return formatting.status('ok', caller_id=caller_id)
def modify_forward(params_dict): # params_dict["forward"] has already been validated params_dict["forward"].update({'last_modified': timing.get_current_time()}) helper_accounts_collection.update_one( {'email': params_dict["email"]}, {'$set': { 'forward': params_dict["forward"] }}) return formatting.status("ok")
def confirm(email): # this function can be used for the initial send as well as resending record = phone_tokens_collection.find_one({'email': email}, { '_id': 0, 'phone_number': 1 }) if record is None: return formatting.status('not verification found'), 400 if record['phone_number'] == '': return formatting.status('not verification found'), 400 helper_accounts_collection.update_one({'email': email}, { '$set': { 'phone_number': record['phone_number'], 'phone_number_verified': True } }) phone_tokens_collection.delete_many({'email': email}) return formatting.status("ok"), 200
def modify_call(params_dict): # Step 1) Check database correctness helper = helper_accounts_collection.find_one( {"email": params_dict["email"]}) if helper is None: return formatting.server_error_helper_record call = calls_collection.find_one( {"_id": ObjectId(params_dict['call']["call_id"])}) if call is None: return formatting.status("call_id invalid") # Step 2) Check eligibility to modify this call if str(call["helper_id"]) != str(helper["_id"]): return formatting.status("not authorized to edit this call") if (call["status"] == "fulfilled") and (params_dict['call']["action"] in ["reject", "fulfill"]): return formatting.status('cannot change a fulfilled call') # Step 2) Actually edit the call if params_dict['call']["action"] == "fulfill": fulfill_call(params_dict['call']["call_id"], helper["_id"]) elif params_dict['call']["action"] == "reject": reject_call(params_dict['call']["call_id"], helper["_id"]) elif params_dict['call']["action"] == "comment": comment_call(params_dict['call']["call_id"], params_dict['call']["comment"]) return formatting.status("ok")
def route_database_fetchall(api_version): if api_version == "v1": params_dict = routing.get_params_dict(request) authentication_result = tokening.check_helper_api_key( params_dict, new_api_key=(os.getenv("ENVIRONMENT") == "production")) if authentication_result["status"] != "ok": return formatting.postprocess_response(authentication_result) account_dict = account_scripts.get_account(params_dict['email']) calls_dict = call_scripts.get_calls(params_dict['email']) filter_dict = filter_scripts.get_filter(params_dict['email']) forward_dict = forward_scripts.get_forward(params_dict['email']) for result_dict in [ account_dict, calls_dict, filter_dict, forward_dict ]: if result_dict["status"] != "ok": return formatting.postprocess_response(result_dict) performance_dict = performance_scripts.get_performance( account_dict["account"]["zip_code"]) result_dict = formatting.status( "ok", account=account_dict["account"], calls=calls_dict["calls"], filter=filter_dict["filter"], forward=forward_dict["forward"], performance=performance_dict["performance"]) return formatting.postprocess_response( result_dict, new_api_key=authentication_result['api_key']) else: return formatting.status("api_version invalid"), 400
def route_helper_phone_trigger(api_version): if api_version == "v1": params_dict = routing.get_params_dict(request) authentication_result = tokening.check_helper_api_key(params_dict) if authentication_result["status"] != "ok": return formatting.postprocess_response(authentication_result) result_dict = phone_verification.trigger(params_dict['email']) return formatting.postprocess_response(result_dict) else: # Programming Error return formatting.status("api_version invalid"), 400
def trigger(email): phone_tokens_collection.delete_many({ '$or': [{ '$and': [ { 'timestamp_issued': { '$lt': timing.get_current_time(offset_minutes=-3) } }, { 'phone_number': '' }, ] }, { 'email': email }] }) # Generate new token # By including the existing tokens a duplicate token error is impossible, # however the token generation might take longer and is non-deterministic existing_tokens = [ document['token'] for document in list( phone_tokens_collection.find({}, { '_id': 0, 'token': 1 })) ] token = tokening.generate_random_key(length=5, numeric=True, existing_tokens=existing_tokens) # Create new token record new_record = { 'email': email, 'token': token, 'timestamp_issued': timing.get_current_time(), 'phone_number': '', } phone_tokens_collection.insert_one(new_record) # Trigger token-email return formatting.status('ok', token=token)
def trigger(email): # this function can be used for the initial send as well as resending helper_account = helper_accounts_collection.find_one({'email': email}) if helper_account['account']['email_verified']: return formatting.status('email already verified') # Generate new token verification_token = tokening.generate_random_key(length=64) helper_id = ObjectId(helper_account["_id"]) # Create new token record record = {'helper_id': helper_id, 'token': verification_token} operations = [DeleteMany({'helper_id': helper_id}), InsertOne(record)] email_tokens_collection.bulk_write(operations, ordered=True) # Trigger token-email return send(email, verification_token)
def add_call(caller_id, language, call_type='', zip_code=''): current_timestamp = timing.get_current_time() # local is boolean new_call = { 'caller_id': ObjectId(caller_id), 'call_type': [call_type], 'zip_code': zip_code, 'language': language, 'feedback_granted': False, 'confirmed': False, 'helper_id': 0, 'status': 'pending', 'comment': '', 'timestamp_received': current_timestamp, 'timestamp_accepted': current_timestamp, 'timestamp_fulfilled': current_timestamp, } call_id = calls_collection.insert_one(new_call).inserted_id return formatting.status('ok', call_id=call_id)
def validate_edit_account(params_dict): if "account" not in params_dict: return formatting.status("account missing") return validate(params_dict["account"], edit_account_validator)
def validate_edit_call(params_dict): if "call" not in params_dict: return formatting.status("call missing") return validate(params_dict["call"], edit_call_validator)
def dequeue(helper_id, zip_code=None, only_local=None, only_global=None, german=None, english=None): current_timestamp = timing.get_current_time() if only_local and only_global: return formatting.status( 'invalid function call - only_local = only_global = True') # Step 1) Find the helpers zip_code if any([e is None] for e in [zip_code, only_local, only_global, english, german]) is None: helper = helper_accounts_collection.find_one( {'_id': ObjectId(helper_id)}) if helper is None: return formatting.status('helper_id invalid') zip_code = helper['zip_code'] if (zip_code is None) else zip_code only_local = helper['filter_type_local'] if ( only_local is None) else only_local only_global = helper['filter_type_global'] if ( only_global is None) else only_global german = helper['filter_language_german'] if ( german is None) else german english = helper['filter_language_english'] if ( english is None) else english language_list = [] language_list += ['german'] if german else [] language_list += ['english'] if english else [] zip_codes_list = fetching.get_adjacent_zip_codes(zip_code) if ( zip_code != '') else [] projection_dict = {} # Step 2) Find Call if only_local: filter_dict = { 'call_type': { "$elemMatch": { "$eq": 'local' } }, 'zip_code': { '$in': zip_codes_list }, 'language': { '$in': language_list } } call = call_queue.find_one_and_delete( filter_dict, projection_dict, sort=[('timestamp_received', 1)], ) elif only_global: filter_dict = { 'call_type': { "$elemMatch": { "$eq": 'global' } }, 'language': { '$in': language_list } } call = call_queue.find_one_and_delete( filter_dict, projection_dict, sort=[('timestamp_received', 1)], ) else: # 1. Urgent Queue filter_dict = { 'timestamp_received': { '$lt': timing.get_current_time(offset_seconds=-global_timeout_seconds) } } call = call_queue.find_one_and_delete( filter_dict, projection_dict, sort=[('timestamp_received', 1)], ) # 2. Local Queue if call is None: filter_dict = { 'call_type': { "$elemMatch": { "$eq": 'local' } }, 'zip_code': { '$in': zip_codes_list }, 'language': { '$in': language_list } } call = call_queue.find_one_and_delete( filter_dict, projection_dict, sort=[('timestamp_received', 1)], ) # 3. Global Queue if call is None: # Or chain needed so that calls in other regions which # are not in the global queue yet get assigned filter_dict = { '$or': [{ 'call_type': { "$elemMatch": { "$eq": 'local' } }, 'timestamp_received': { '$lt': timing.get_current_time( offset_seconds=-local_timeout_seconds) } }, { 'call_type': { "$elemMatch": { "$eq": 'global' } } }] }, call = call_queue.find_one_and_delete( filter_dict, projection_dict, sort=[('timestamp_received', 1)], ) if call is None: return formatting.status('currently no call available') call_id = call['call_id'] # Step 3) Update call (helper_id, formatting.status, timestamp_accepted) call_update_dict_1 = { "$set": { 'status': 'accepted', 'helper_id': ObjectId(helper_id), 'timestamp_accepted': current_timestamp } } print(f"call = {call}") # accepted-match if local call was accepted from local queue (successful) or global call # accepted-mismatch if local call was matched with non-local helper if "local" in call["call_type"] and call["zip_code"] not in zip_codes_list: new_call_type = "accepted-mismatch" else: new_call_type = "accepted-match" call_update_dict_2 = { "$push": { "call_type": new_call_type, } } operations = [ UpdateOne({'_id': ObjectId(call_id)}, call_update_dict_1), UpdateOne({'_id': ObjectId(call_id)}, call_update_dict_2) ] calls_collection.bulk_write(operations) # Step 4) Add helper behavior (helper_id, call_id, timestamp, action='accepted' new_behavior_log = { 'helper_id': ObjectId(helper_id), 'call_id': ObjectId(call_id), 'timestamp': current_timestamp, 'action': 'accepted', } helper_behavior_collection.insert_one(new_behavior_log) return formatting.status('ok')