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
Пример #2
0
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')
Пример #3
0
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
Пример #4
0
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')
Пример #5
0
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'])
Пример #6
0
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')
Пример #7
0
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')
Пример #8
0
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')
Пример #9
0
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
Пример #10
0
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
Пример #11
0
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")
Пример #12
0
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'])
Пример #13
0
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
Пример #15
0
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')
Пример #16
0
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')
Пример #17
0
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")
Пример #18
0
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)
Пример #19
0
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)
Пример #20
0
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")
Пример #21
0
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
Пример #22
0
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")
Пример #23
0
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
Пример #24
0
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
Пример #25
0
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)
Пример #26
0
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)
Пример #27
0
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)
Пример #28
0
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)
Пример #29
0
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)
Пример #30
0
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')