# By: Riasat Ullah
# This file contains all the views related to a user.

from data_syncers import syncer_users
from dbqueries import db_accounts, db_organizations, db_policies, db_users
from exceptions.user_exceptions import InvalidPassword, InvalidRequest, LockedAccount, MaliciousSource,\
    UnauthorizedRequest
from google.oauth2 import id_token
from google.auth.transport import requests as google_requests
from msal import oauth2cli
from modules import credentials, on_call_manager
from notices import account_notices
from rest_framework.decorators import api_view
from rest_framework.response import Response
from taskcallrest import settings
from translators import label_translator as _lt
from utils import constants, errors, helpers, integration_type_names as intt, key_manager, logging, mail, \
    permissions, sso_manager, times, tokenizer, tutorial_names, var_names
from utils.db_connection import CACHE_CLIENT, CONN_POOL
from utils import ban_checker, info
from validations import request_validator
from validations import string_validator
import configuration as configs
import datetime
import jwt


@api_view(['POST'])
def login(request, conn=None, cache=None):
    '''
    This function allows a user to login.
    :param request: Http request
    :param conn: db connection
    :param cache: cache client
    :return: Http response -> token
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        direct_login_required_fields = [var_names.email, var_names.password, var_names.access_method]
        google_sso_required_fields = [var_names.integration_type, var_names.id_token, var_names.subdomain,
                                      var_names.access_method]
        azure_sso_required_fields = [var_names.integration_type, var_names.id_token, var_names.subdomain,
                                     var_names.access_method]
        okta_sso_required_fields = [var_names.integration_type, var_names.id_token, var_names.vendor_endpoint,
                                    var_names.subdomain, var_names.access_method]
        saml_sso_required_fields = [var_names.integration_type, var_names.email, var_names.subdomain,
                                    var_names.access_method]
        optional_fields = [var_names.push_token, var_names.session_id, var_names.ip_address]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            is_user_authenticated = False
            sso_type = None

            ################################
            # Validate the request first
            ################################
            if var_names.integration_type not in request.data:
                request_validator.validate_fields(request, direct_login_required_fields, optional_fields)
            else:
                sso_type = request.data[var_names.integration_type]
                if sso_type not in configs.allowed_sso_types:
                    raise InvalidRequest(errors.err_invalid_request)

                if sso_type == intt.google_sso:
                    request_validator.validate_fields(request, google_sso_required_fields, optional_fields)
                elif sso_type == intt.azure_ad_sso:
                    request_validator.validate_fields(request, azure_sso_required_fields, optional_fields)
                elif sso_type == intt.okta:
                    request_validator.validate_fields(request, okta_sso_required_fields, optional_fields)
                elif sso_type == intt.saml:
                    request_validator.validate_fields(request, saml_sso_required_fields, optional_fields)
                else:
                    raise InvalidRequest(errors.err_invalid_request)

            access_method = request.data[var_names.access_method]
            push_token = request.data[var_names.push_token] if var_names.push_token in request.data else None
            session_id = request.data[var_names.session_id] if var_names.session_id in request.data else None
            ip_address = request.data[var_names.ip_address]\
                if var_names.ip_address in request.data else request.META.get(constants.ip_address_attribute)

            # Throw an error if web login requests do not have a session id. It is fine if mobile app
            # login does not have a push token because users can choose to not give push token permission.
            if access_method == constants.web and session_id is None:
                raise InvalidRequest(errors.err_invalid_request)

            #########################################################
            # Now process the request and authenticate correctly
            #########################################################
            current_time = times.get_current_timestamp()
            if sso_type is None:
                email = request.data[var_names.email].lower()
                password = request.data[var_names.password]
                access_method = request.data[var_names.access_method]

                user_id, allow_direct_login = db_users.get_user_id_and_login_rights_from_email(
                    conn, current_time, email)

                # This is for organizations that have SSO. They can enforce login to only be allowed through their IdP.
                if not allow_direct_login:
                    return Response(_lt.get_label(errors.err_user_login_requires_sso, lang), status=403)

                is_user_authenticated = credentials.user_login(conn, user_id, password, access_method,
                                                               ip_address, current_time)
            else:
                if sso_type == intt.google_sso:
                    access_method = request.data[var_names.access_method]
                    tc_subdomain = request.data[var_names.subdomain]
                    google_token = request.data[var_names.id_token]

                    google_sso_creds = sso_manager.get_sso_credentials(intt.google_sso)
                    id_info = id_token.verify_oauth2_token(google_token, google_requests.Request(),
                                                           google_sso_creds[var_names.client_id])
                    email = id_info['email']
                elif sso_type == intt.azure_ad_sso:
                    access_method = request.data[var_names.access_method]
                    tc_subdomain = request.data[var_names.subdomain]
                    azure_token = request.data[var_names.id_token]

                    azure_sso_creds = sso_manager.get_sso_credentials(intt.azure_ad_sso)
                    id_info = oauth2cli.oidc.decode_id_token(azure_token, azure_sso_creds[var_names.client_id])
                    email = id_info['email']
                elif sso_type == intt.okta:
                    access_method = request.data[var_names.access_method]
                    tc_subdomain = request.data[var_names.subdomain]
                    okta_token = request.data[var_names.id_token]
                    org_sso_config = db_organizations.get_organization_sso_settings(conn, current_time, tc_subdomain)
                    if org_sso_config[var_names.integration_type] == intt.okta:
                        inspect_data = {
                            'client_id': org_sso_config[var_names.additional_info][var_names.client_id],
                            'client_secret': org_sso_config[var_names.additional_info][var_names.client_secret],
                            'token': okta_token,
                            'token_type_hint': 'id_token'
                        }
                        id_status, id_info = helpers.post_api_request(
                            request.data[var_names.vendor_endpoint], inspect_data,
                            content_type=constants.content_type_url_encoded
                        )
                        if id_status == 200:
                            email = id_info['email']
                        else:
                            raise InvalidRequest(errors.err_invalid_request)
                    else:
                        raise InvalidRequest(errors.err_invalid_request)
                elif sso_type == intt.saml:
                    access_method = request.data[var_names.access_method]
                    tc_subdomain = request.data[var_names.subdomain]
                    org_sso_config = db_organizations.get_organization_sso_settings(conn, current_time, tc_subdomain)
                    if org_sso_config[var_names.integration_type] == intt.saml:
                        email = request.data[var_names.email]
                    else:
                        raise InvalidRequest(errors.err_invalid_request)
                else:
                    raise InvalidRequest(errors.err_invalid_request)

                email = email.lower()
                user_id, allow_auto_provision = db_users.get_user_id_and_sso_permits(
                    conn, current_time, tc_subdomain, sso_type, email)
                if user_id is None:
                    if allow_auto_provision:
                        tk_expiry = current_time + datetime.timedelta(minutes=configs.registration_token_lifetime)
                        reg_token = tokenizer.create_registration_token(email, ip_address, tk_expiry)
                        resp = {var_names.email: email, var_names.token: reg_token}
                        return Response(resp, status=307)
                    else:
                        raise UnauthorizedRequest(errors.err_sso_auto_provision_not_allowed)
                else:
                    db_users.store_login_attempt(conn, user_id, current_time, access_method, ip_address, True)
                    is_user_authenticated = True

            if is_user_authenticated:
                org_id, user_perm, org_perm, org_end, display_name, user_lang, photo_url =\
                    db_users.get_user_token_details(conn, current_time, user_id)

                # restrict the permissions allowed to mobile app access tokens
                if access_method == constants.app:
                    org_perm = permissions.create_mobile_app_access_token_permissions(org_perm)

                # Get the expiry dates for the access and refresh tokens.
                access_end = tokenizer.new_access_token_expiry_date(org_end)
                refresh_end = tokenizer.new_refresh_token_expiry_date(access_method)

                access_token = tokenizer.create_token(user_id, org_id, user_perm, org_perm, access_end)
                refresh_token = tokenizer.create_refresh_token(user_id, org_id, access_method, refresh_end)

                db_users.save_authentication_tokens(conn, current_time, access_end, user_id, org_id, access_method,
                                                    access_token, refresh_token, push_token, session_id)

                # Under the new login process for mobile apps, there is no way to get the email directly.
                # Hence, it is also sent from here. The email value is used in saving account tokens.
                response_data = {
                    var_names.access_token: access_token,
                    var_names.expires_on: access_end,
                    var_names.refresh_token: refresh_token,
                    var_names.nav_bar_components: permissions.get_user_viewable_components(user_perm, org_perm),
                    var_names.component_features: permissions.get_user_accessible_component_features(
                        user_perm, org_perm),
                    var_names.edit_permissions: permissions.get_user_editable_components(user_perm, org_perm),
                    var_names.has_team_permission: permissions.has_org_permission(
                        org_perm, permissions.ORG_TEAMS_PERMISSION),
                    var_names.display_name: display_name,
                    var_names.language: user_lang,
                    var_names.profile_picture: photo_url,
                    var_names.email: email,
                    var_names.host_region: settings.REGION
                }

                # Sync up the cache with new push tokens if the user's dispatch info is in cache.
                if access_method == constants.app and push_token is not None:
                    user_obj = db_users.get_user(conn, current_time, user_id=user_id)[user_id]
                    if user_obj.policy_id is not None:
                        cache = CACHE_CLIENT if cache is None else cache
                        syncer_users.update_user_policy_info(conn, cache, times.get_current_timestamp(),
                                                             user_obj.policy_id)

                return Response(response_data)
            else:
                return Response(_lt.get_label(errors.err_invalid_email_or_password, lang), status=401)
        except (InvalidRequest, KeyError, LookupError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except (LockedAccount, UnauthorizedRequest) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=403)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def logout(request, conn=None):
    '''
    Logs out a user from his account on a device.
    :param request: Http request
    :param conn: db connection
    :return: Http response -> str
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            user_id = tokenizer.authorize_request(request)[0]
            token = tokenizer.extract_token(request)
            db_users.delete_authentication_tokens(conn, times.get_current_timestamp(), user_id, token)
            return Response(_lt.get_label(info.msg_success, lang))
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def refresh_access_token(request, conn=None):
    '''
    Refreshes access by issuing a new access token.
    :param request: Http request
    :param conn: db connection
    :return: Http response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.refresh_token]
        optional_fields = [var_names.push_token, var_names.session_id]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            request_validator.validate_fields(request, expected_fields, optional_fields)
            access_token = tokenizer.extract_token(request)
            refresh_token = request.data[var_names.refresh_token]
            push_token = request.data[var_names.push_token] if var_names.push_token in request.data else None
            session_id = request.data[var_names.session_id] if var_names.session_id in request.data else None
            if push_token is None and session_id is None:
                raise InvalidRequest(errors.err_invalid_request)

            user_id, org_id, access_method = tokenizer.read_refresh_token(request.data[var_names.refresh_token])
            current_time = times.get_current_timestamp()

            # Apart from checking if the refresh token is valid, the query also ensures
            # that the access token has expired. It will return False if it has not.
            if db_users.is_valid_refresh_token(conn, current_time, refresh_token, access_token, user_id, org_id):
                org_id, user_perm, org_perm, org_end, display_name, user_lang, photo_url =\
                    db_users.get_user_token_details(conn, current_time, user_id)

                # Get the expiry dates for the access and refresh tokens.
                access_end = tokenizer.new_access_token_expiry_date(org_end)
                access_token = tokenizer.create_token(user_id, org_id, user_perm, org_perm, access_end)

                db_users.save_authentication_tokens(conn, current_time, access_end, user_id, org_id, access_method,
                                                    access_token, refresh_token, push_token, session_id)
                response_data = {var_names.access_token: access_token,
                                 var_names.expires_on: access_end,
                                 var_names.refresh_token: refresh_token}
                return Response(response_data)
            else:
                raise UnauthorizedRequest(errors.err_authorization)
        except InvalidRequest as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def get_user_environment(request, conn=None):
    '''
    This function gets the environment of a user's mobile app.
    :param request: Http request
    :param conn: db connection
    :return: Http response -> token
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.access_method]
        optional_fields = [var_names.email]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            request_validator.validate_fields(request, expected_fields, optional_fields)
            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)

            access_method = request.data[var_names.access_method]
            email = request.data[var_names.email].lower() if var_names.email in request.data else None

            # Get the end time of the refresh token.
            current_time = times.get_current_timestamp()
            if email is not None:
                user_id = db_users.get_user_id_and_login_rights_from_email(conn, current_time, email)[0]

            org_id, user_perm, org_perm, org_end, display_name, user_lang, photo_url =\
                db_users.get_user_token_details(conn, current_time, user_id)

            # restrict the permissions allowed to mobile app access tokens
            if access_method == constants.app:
                org_perm = permissions.create_mobile_app_access_token_permissions(org_perm)

            response_data = {
                var_names.nav_bar_components: permissions.get_user_viewable_components(user_perm, org_perm),
                var_names.component_features: permissions.get_user_accessible_component_features(
                    user_perm, org_perm),
                var_names.edit_permissions: permissions.get_user_editable_components(user_perm, org_perm),
                var_names.has_team_permission: permissions.has_org_permission(
                    org_perm, permissions.ORG_TEAMS_PERMISSION),
                var_names.display_name: display_name,
                var_names.language: user_lang,
                var_names.profile_picture: photo_url,
                var_names.host_region: settings.REGION
            }
            return Response(response_data)
        except (InvalidRequest, KeyError, LookupError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except LockedAccount as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=403)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def user_display_name(request, conn=None):
    '''
    Get a user's display name.
    :param request: Http request
    :param conn: db connection
    :return: Http response -> dict
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.preferred_username]
        try:
            request_validator.validate_fields(request, expected_fields)
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)

            if permissions.has_user_permission(user_perm, permissions.USER_ORG_MEMBERS_VIEW_PERMISSION):
                retrieve_for = request.data[var_names.preferred_username]
                current_time = times.get_current_timestamp()

                if retrieve_for is None:
                    display_name = db_users.get_user_display_name(conn, current_time, org_id, user_id=user_id)
                else:
                    display_name = db_users.get_user_display_name(conn, current_time, org_id,
                                                                  pref_username=retrieve_for)
                return Response(display_name)
            else:
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)
        except (InvalidRequest, LookupError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def user_public_profile(request, conn=None):
    '''
    Get a user's profile.
    :param request: Http request
    :param conn: db connection
    :return: Http response -> dict
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.preferred_username]
        try:
            request_validator.validate_fields(request, expected_fields)
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)

            if permissions.has_user_permission(user_perm, permissions.USER_ORG_MEMBERS_VIEW_PERMISSION):
                retrieve_for = request.data[var_names.preferred_username]
                profile = db_users.get_user_public_profile(conn, times.get_current_timestamp(), org_id, retrieve_for)
                return Response(profile)
            else:
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)
        except (InvalidRequest, LookupError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def user_profile(request, conn=None):
    '''
    Get a user's profile.
    :param request: Http request
    :param conn: db connection
    :return: Http response -> dict
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.preferred_username]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            request_validator.validate_fields(request, expected_fields)
            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)

            pref_name = request.data[var_names.preferred_username]
            current_time = times.get_current_timestamp()

            if pref_name is None or\
                    permissions.has_user_permission(user_perm, permissions.USER_ORG_MEMBERS_VIEW_PERMISSION):

                profile = db_users.get_user_details(conn, current_time, org_id,
                                                    user_id=user_id if pref_name is None else None,
                                                    preferred_username=pref_name if pref_name is not None else None)
                return Response(profile)
            else:
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def update_profile(request, conn=None, cache=None):
    '''
    Update a user's profile.
    :param request: Http request
    :param conn: db connection
    :param cache: cache client
    :return: Http response -> str
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.preferred_username, var_names.first_name, var_names.last_name,
                           var_names.iso_country_code, var_names.phone, var_names.timezone,
                           var_names.language, var_names.job_title]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            cache = CACHE_CLIENT if cache is None else cache
            request_validator.validate_fields(request, expected_fields)
            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)

            current_time = times.get_current_timestamp()
            pref_name = request.data[var_names.preferred_username]
            user_obj = list(db_users.get_user(conn, current_time, organization_id=org_id,
                                              preferred_username=pref_name).values())[0]

            if user_id == user_obj.user_id or permissions.is_user_admin(user_perm):

                db_users.update_user_details(conn, current_time, org_id, user_obj.user_id,
                                             request.data[var_names.first_name].title(),
                                             request.data[var_names.last_name].title(),
                                             request.data[var_names.iso_country_code],
                                             request.data[var_names.phone],
                                             request.data[var_names.timezone],
                                             request.data[var_names.language],
                                             request.data[var_names.job_title])

                # We do this check because stakeholders do not have policy IDs
                # and so do not have any dispatch info stored in cache.
                if user_obj.policy_id is not None:
                    syncer_users.update_user_policy_info(conn, cache, times.get_current_timestamp(), user_obj.policy_id)

                return Response(_lt.get_label(info.msg_success, lang))
            else:
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)
        except InvalidRequest as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def update_profile_picture(request, conn=None):
    '''
    Update a user's profile picture only.
    :param request: Http request
    :param conn: db connection
    :return: Http response -> str
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.preferred_username, var_names.profile_picture]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            request_validator.validate_fields(request, expected_fields)
            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)

            current_time = times.get_current_timestamp()
            pref_name = request.data[var_names.preferred_username]
            user_obj = list(db_users.get_user(conn, current_time, organization_id=org_id,
                                              preferred_username=pref_name).values())[0]

            if user_id == user_obj.user_id or permissions.is_user_admin(user_perm):

                db_users.update_user_profile_picture(conn, current_time, org_id, user_obj.user_id,
                                                     request.data[var_names.profile_picture])

                return Response(_lt.get_label(info.msg_success, lang))
            else:
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)
        except InvalidRequest as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def forgot_password(request, conn=None):
    '''
    First stage of a password reset attempt. Checks if the provided details match or not.
    :param request: Http Request
    :param conn: db connection
    :return: HttpResponse -> token
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.email]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            request_validator.validate_fields(request, expected_fields)

            email = request.data[var_names.email]
            ip_address = request.META.get(constants.ip_address_attribute)
            current_time = times.get_current_timestamp()

            email = email.lower()
            user_id = db_users.get_user_id_and_login_rights_from_email(conn, current_time, email)[0]

            if credentials.verify_password_reset_attempt(conn, current_time, user_id, ip_address):

                # first - generate the verification code
                code = key_manager.generate_verification_code(constants.verification_code_length)
                code_hash, salt = credentials.convert_text_to_hash(code)

                # second - save the code in the database
                start_timestamp = times.get_current_timestamp()
                end_timestamp = start_timestamp + datetime.timedelta(minutes=configs.password_reset_token_lifetime)

                db_users.save_personal_account_verification_code(conn, start_timestamp, end_timestamp, email, salt,
                                                                 code_hash, constants.password_reset_verification)

                # third - email the code
                subject, message = account_notices.password_reset_verification_content(lang, email, code)
                mail.AmazonSesDispatcher(subject, message, email, with_logo=True).start()

                # A temp token is sent to be able to verify who the request is coming from in the next stage
                # where the verification code is confirmed back. The token only has the permission to reset
                # a forgotten password with a very short lifetime. It is also saved as a temporary token on
                # the web server. Hence, the token cannot be used for any other purpose.
                token_perm = permissions.create_password_reset_token_permission()
                reset_token = tokenizer.create_password_reset_token(user_id, token_perm, expiry_date=end_timestamp)
                return Response(reset_token)
            else:
                return Response(_lt.get_label(errors.err_verification_failed, lang), status=403)
        except (InvalidRequest, LookupError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def verify_forgotten_password_reset_attempt(request, conn=None):
    '''
    Verifies if a password reset attempt was made by the user itself or not.
    :param request: Http request
    :param conn: db connection
    :return: Http response -> str
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.verification_code]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            request_validator.validate_fields(request, expected_fields)
            user_id, token_perm = tokenizer.read_password_reset_token(tokenizer.extract_token(request))
            code = request.data[var_names.verification_code]

            current_time = times.get_current_timestamp()
            user_email = db_users.get_user(conn, current_time, user_id=user_id)[user_id].email

            if permissions.has_user_permission(token_perm, permissions.USER_PASSWORD_RESET_PERMISSION) and\
                    credentials.verification_code_matches(conn, times.get_current_timestamp(), user_email, code,
                                                          verification_type=constants.password_reset_verification):
                return Response(_lt.get_label(info.msg_success, lang))
            else:
                return Response(_lt.get_label(errors.err_verification_failed, lang), status=403)
        except InvalidRequest as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except KeyError as e:
            # KeyErrors will get generated if the user id in the temp token does not have a corresponding email
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_verification_failed, lang), status=401)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def reset_forgotten_password(request, conn=None):
    '''
    Edit a user's password. This should be used when the user wants to actually update the password;
    not when the user has forgotten the password. When the user wants to update the password when it
    has forgotten it, please user the forgot_password function.
    :param request: Http request
    :param conn: db connection
    :return: Http response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.password]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            request_validator.validate_fields(request, expected_fields)
            user_id, token_perm = tokenizer.read_password_reset_token(tokenizer.extract_token(request))
            new_password = request.data[var_names.password]
            current_time = times.get_current_timestamp()

            # Ensure that the password reset attempt was verified. This allows us to stop the reset
            # succeeding if the user hadn't actually verified the code in the prior step.
            user_email = db_users.get_user(conn, current_time, user_id=user_id)[user_id].email
            was_reset_attempt_verified = db_users.was_password_reset_attempt_verified(conn, current_time, user_email)

            if permissions.has_user_permission(token_perm, permissions.USER_PASSWORD_RESET_PERMISSION) and\
                    was_reset_attempt_verified:
                db_users.reset_password(conn, user_id, new_password, current_time)
                return Response(_lt.get_label(info.msg_success, lang))
            else:
                return Response(_lt.get_label(errors.err_verification_failed, lang), status=403)
        except (InvalidRequest, InvalidPassword) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except KeyError as e:
            # KeyErrors will get generated if the user id in the temp token does not have a corresponding email
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_verification_failed, lang), status=401)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def reset_password(request, conn=None):
    '''
    Resets a user's password. This function only works for passwords resets being made when the user is logged in.
    A user can only change their own password; even owners and admins cannot change anyone else's password.
    :param request: Http request
    :param conn: db connection
    :return: Http response -> str
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.password]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            request_validator.validate_fields(request, expected_fields)
            user_id = tokenizer.authorize_request(request)[0]
            new_password = request.data[var_names.password]

            db_users.reset_password(conn, user_id, new_password, times.get_current_timestamp())
            return Response(_lt.get_label(info.msg_success, lang))
        except (InvalidRequest, InvalidPassword) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def forgot_subdomain(request, conn=None):
    '''
    This is for SSO. If users forget their organization TaskCall subdomain,
    they can request for it to be emailed to them.
    :param request: Http Request
    :param conn: db connection
    :return: HttpResponse
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.email]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            request_validator.validate_fields(request, expected_fields)

            email = request.data[var_names.email]
            email = email.lower()
            subdomain = db_users.get_user_organization_subdomain(conn, times.get_current_timestamp(), email)

            subject, message = account_notices.forgot_subdomain_content(lang, subdomain)
            mail.AmazonSesDispatcher(subject, message, email, with_logo=True).start()
            return Response(_lt.get_label(info.msg_subdomain_emailed, lang))
        except (InvalidRequest, LookupError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def email_is_unique(request, conn=None):
    '''
    Checks if an email account is unique in the system or not. This will help to make sure
    that duplicate accounts are not allowed to be created under the same email address.
    :param request: Http request
    :param conn: db connection
    :return: Http response -> str
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.email]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            request_validator.validate_fields(request, expected_fields)
            email = request.data[var_names.email].lower()

            if not string_validator.is_email_address(email):
                return Response(_lt.get_label(errors.err_invalid_email, lang), status=400)

            logging.info('Checking for unique email...')
            if ban_checker.is_source_malicious(conn, [(var_names.subdomain, email.split('@')[1])]):
                raise MaliciousSource(errors.err_malicious_source_restriction)

            if db_users.email_is_unique(conn, email, times.get_current_timestamp()):
                return Response(_lt.get_label(info.msg_success, lang), status=200)
            else:
                return Response(_lt.get_label(errors.err_requested_user_account_exists, lang), status=400)
        except InvalidRequest as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except UnauthorizedRequest as e:
            logging.info(str(e))
            return Response(_lt.get_label(errors.err_invalid_request, lang), status=401)
        except MaliciousSource as e:
            logging.info(str(e))
            return Response(_lt.get_label(errors.err_malicious_source_restriction, lang), status=403)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def create_new_account_verification_code_for_owner(request, conn=None):
    '''
    Create and save the verification code for a new account.
    :param request: Http request
    :param conn: db connection
    :return: Http response -> str
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.email]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            request_validator.validate_fields(request, expected_fields)
            email = request.data[var_names.email]

            # first - generate the verification code
            code = key_manager.generate_verification_code(constants.verification_code_length)
            code_hash, salt = credentials.convert_text_to_hash(code)

            # second - save the code in the database
            start_timestamp = times.get_current_timestamp()
            end_timestamp = start_timestamp + datetime.timedelta(hours=configs.new_account_code_life_hours)

            db_users.save_personal_account_verification_code(conn, start_timestamp, end_timestamp,
                                                             email, salt, code_hash)

            # third - email the code
            subject, message = account_notices.new_account_verification_content(lang, email, code)
            mail.AmazonSesDispatcher(subject, message, email, with_logo=True).start()
            return Response(_lt.get_label(info.msg_success, lang))
        except InvalidRequest as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def verify_new_account(request, conn=None):
    '''
    Checks if a new account verifies or not.
    :param request: Http request
    :param conn: db connection
    :return: Http response -> sends a token which must be sent with
            the registration details to complete the registration
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.email, var_names.verification_code, var_names.ip_address]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            request_validator.validate_fields(request, expected_fields)
            email = request.data[var_names.email]
            code = request.data[var_names.verification_code]
            ip_addr = request.data[var_names.ip_address]

            current_time = times.get_current_timestamp()
            if credentials.verification_code_matches(conn, current_time, email, code):

                tk_expiry = current_time + datetime.timedelta(minutes=configs.registration_token_lifetime)
                reg_token = tokenizer.create_registration_token(email, ip_addr, tk_expiry)

                return Response(reg_token)
            else:
                return Response(_lt.get_label(errors.err_verification_failed, lang), status=400)
        except InvalidRequest as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except KeyError as e:
            # KeyError is raise from db_users.get_verification_code if an active token does not exist;
            # meaning that it might have expired or one might have never been requested
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_verification_expired, lang), status=403)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def get_alert_notification_rules(request, conn=None):
    '''
    Get the alert notification rules of a user.
    :param request: Http request
    :param conn: db connection
    :return: JSON response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.preferred_username]
        try:
            request_validator.validate_fields(request, expected_fields)
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)
            current_time = times.get_current_timestamp()
            pref_name = request.data[var_names.preferred_username]

            if pref_name is None:
                for_user = db_users.get_user(conn, current_time, user_id=user_id, organization_id=org_id)[user_id]
            else:
                for_user = db_users.get_user(conn, current_time, keyed_on=var_names.preferred_username,
                                             preferred_username=pref_name, organization_id=org_id)[pref_name]

            # check if the user has permission to view other member's details or is requesting to see his own
            if permissions.has_user_permission(user_perm, permissions.USER_ORG_MEMBERS_VIEW_PERMISSION) or\
                    for_user.user_id == user_id:

                data = list(db_users.get_alert_notification_rules(
                    conn, current_time, user_id=for_user.user_id).values())
                if len(data) > 0:
                    data = data[0]
                return Response(data)
            else:
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def create_alert_notification_rule(request, conn=None, cache=None):
    '''
    Create a new alert notification rule.
    :param request: Http request
    :param conn: db connection
    :param cache: cache client
    :return: JSON response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.preferred_username, var_names.urgency_level, var_names.notification_rules]
        try:
            request_validator.validate_fields(request, expected_fields)
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            cache = CACHE_CLIENT if cache is None else cache

            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)
            current_time = times.get_current_timestamp()
            pref_name = request.data[var_names.preferred_username]
            for_user = db_users.get_user(conn, current_time, keyed_on=var_names.preferred_username,
                                         preferred_username=pref_name, organization_id=org_id)[pref_name]

            if permissions.has_user_permission(user_perm, permissions.USER_ORG_MEMBERS_EDIT_PERMISSION) or \
                    for_user.user_id == user_id:

                notf_rules = request.data[var_names.notification_rules]
                new_notf_rules = dict()
                for buff_min in notf_rules:
                    new_notf_rules[int(buff_min)] = notf_rules[buff_min]

                new_rule_id = db_users.create_alert_notification_rule(conn, current_time, for_user.user_id,
                                                                      request.data[var_names.urgency_level],
                                                                      new_notf_rules)

                if for_user.policy_id is not None:
                    syncer_users.update_user_policy_info(conn, cache, times.get_current_timestamp(), for_user.policy_id)

                return Response(new_rule_id)
            else:
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)
        except LookupError as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=401)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def edit_alert_notification_rule(request, conn=None, cache=None):
    '''
    Edit an alert notification rule.
    :param request: Http request
    :param conn: db connection
    :param cache: cache client
    :return: JSON response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.notification_rule_id, var_names.preferred_username,
                           var_names.urgency_level, var_names.notification_rules]
        try:
            request_validator.validate_fields(request, expected_fields)
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            cache = CACHE_CLIENT if cache is None else cache

            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)
            current_time = times.get_current_timestamp()
            pref_name = request.data[var_names.preferred_username]
            for_user = db_users.get_user(conn, current_time, keyed_on=var_names.preferred_username,
                                         preferred_username=pref_name, organization_id=org_id)[pref_name]

            if permissions.has_user_permission(user_perm, permissions.USER_ORG_MEMBERS_EDIT_PERMISSION) or \
                    for_user.user_id == user_id:

                notf_rules = request.data[var_names.notification_rules]
                new_notf_rules = dict()
                for buff_min in notf_rules:
                    new_notf_rules[int(buff_min)] = notf_rules[buff_min]

                new_rule_id = db_users.edit_alert_notification_rule(
                    conn, current_time, request.data[var_names.notification_rule_id], for_user.user_id,
                    request.data[var_names.urgency_level], new_notf_rules
                )

                if for_user.policy_id is not None:
                    syncer_users.update_user_policy_info(conn, cache, times.get_current_timestamp(), for_user.policy_id)

                return Response(new_rule_id)
            else:
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)
        except LookupError as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=401)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def delete_alert_notification_rule(request, conn=None, cache=None):
    '''
    Delete an alert notification rule.
    :param request: Http request
    :param conn: db connection
    :param cache: cache client
    :return: JSON response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.notification_rule_id, var_names.preferred_username]
        try:
            request_validator.validate_fields(request, expected_fields)
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            cache = CACHE_CLIENT if cache is None else cache

            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)
            current_time = times.get_current_timestamp()
            pref_name = request.data[var_names.preferred_username]
            for_user = db_users.get_user(conn, current_time, keyed_on=var_names.preferred_username,
                                         preferred_username=pref_name, organization_id=org_id)[pref_name]

            if permissions.has_user_permission(user_perm, permissions.USER_ORG_MEMBERS_EDIT_PERMISSION) or \
                    for_user.user_id == user_id:

                db_users.delete_alert_notification_rule(
                    conn, current_time, request.data[var_names.notification_rule_id], for_user.user_id
                )

                if for_user.policy_id is not None:
                   syncer_users.update_user_policy_info(conn, cache, times.get_current_timestamp(), for_user.policy_id)

                return Response(_lt.get_label(info.msg_notification_rule_deleted, lang))
            else:
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def get_hand_off_notification_rules(request, conn=None):
    '''
    Get the alert notification rules of a user.
    :param request: Http request
    :param conn: db connection
    :return: JSON response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.preferred_username]
        try:
            request_validator.validate_fields(request, expected_fields)
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)
            current_time = times.get_current_timestamp()
            pref_name = request.data[var_names.preferred_username]

            if pref_name is None:
                for_user = db_users.get_user(conn, current_time, user_id=user_id, organization_id=org_id)[user_id]
            else:
                for_user = db_users.get_user(conn, current_time, keyed_on=var_names.preferred_username,
                                             preferred_username=pref_name, organization_id=org_id)[pref_name]

            # check if the user has permission to view other member's details or is requesting to see his own
            if permissions.has_user_permission(user_perm, permissions.USER_ORG_MEMBERS_VIEW_PERMISSION) or \
                    for_user.user_id == user_id:

                data = list(db_users.get_hand_off_notification_rules(
                    conn, current_time, user_id=for_user.user_id).values())
                if len(data) > 0:
                    data = data[0]
                return Response(data)
            else:
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def create_hand_off_notification_rule(request, conn=None, cache=None):
    '''
    Create a new hand-off notification rule.
    :param request: Http request
    :param conn: db connection
    :param cache: cache client
    :return: JSON response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.preferred_username, var_names.notification_rules]
        try:
            request_validator.validate_fields(request, expected_fields)
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            cache = CACHE_CLIENT if cache is None else cache

            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)
            current_time = times.get_current_timestamp()
            pref_name = request.data[var_names.preferred_username]
            for_user = db_users.get_user(conn, current_time, keyed_on=var_names.preferred_username,
                                         preferred_username=pref_name, organization_id=org_id)[pref_name]

            if permissions.has_user_permission(user_perm, permissions.USER_ORG_MEMBERS_EDIT_PERMISSION) or \
                    for_user.user_id == user_id:

                notf_rules = request.data[var_names.notification_rules]
                new_notf_rules = dict()
                for buff_min in notf_rules:
                    new_notf_rules[int(buff_min)] = notf_rules[buff_min]

                new_rule_id = db_users.create_hand_off_notification_rule(
                    conn, current_time, for_user.user_id, new_notf_rules)

                if for_user.policy_id is not None:
                    syncer_users.update_user_policy_info(conn, cache, times.get_current_timestamp(), for_user.policy_id)

                return Response(new_rule_id)
            else:
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def edit_hand_off_notification_rule(request, conn=None, cache=None):
    '''
    Edit a hand off notification rule.
    :param request: Http request
    :param conn: db connection
    :param cache: cache client
    :return: JSON response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.notification_rule_id, var_names.preferred_username,
                           var_names.notification_rules]
        try:
            request_validator.validate_fields(request, expected_fields)
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            cache = CACHE_CLIENT if cache is None else cache

            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)
            current_time = times.get_current_timestamp()
            pref_name = request.data[var_names.preferred_username]
            for_user = db_users.get_user(conn, current_time, keyed_on=var_names.preferred_username,
                                         preferred_username=pref_name, organization_id=org_id)[pref_name]

            if permissions.has_user_permission(user_perm, permissions.USER_ORG_MEMBERS_EDIT_PERMISSION) or \
                    for_user.user_id == user_id:

                notf_rules = request.data[var_names.notification_rules]
                new_notf_rules = dict()
                for buff_min in notf_rules:
                    new_notf_rules[int(buff_min)] = notf_rules[buff_min]

                new_rule_id = db_users.edit_hand_off_notification_rule(
                    conn, current_time, request.data[var_names.notification_rule_id], for_user.user_id, new_notf_rules
                )

                if for_user.policy_id is not None:
                    syncer_users.update_user_policy_info(conn, cache, times.get_current_timestamp(), for_user.policy_id)

                return Response(new_rule_id)
            else:
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def delete_hand_off_notification_rule(request, conn=None, cache=None):
    '''
    Delete a hand off notification rule.
    :param request: Http request
    :param conn: db connection
    :param cache: cache client
    :return: JSON response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.notification_rule_id, var_names.preferred_username]
        try:
            request_validator.validate_fields(request, expected_fields)
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            cache = CACHE_CLIENT if cache is None else cache

            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)
            current_time = times.get_current_timestamp()
            pref_name = request.data[var_names.preferred_username]
            for_user = db_users.get_user(conn, current_time, keyed_on=var_names.preferred_username,
                                         preferred_username=pref_name, organization_id=org_id)[pref_name]

            if permissions.has_user_permission(user_perm, permissions.USER_ORG_MEMBERS_EDIT_PERMISSION) or \
                    for_user.user_id == user_id:

                db_users.delete_hand_off_notification_rule(
                    conn, current_time, request.data[var_names.notification_rule_id], for_user.user_id
                )

                if for_user.policy_id is not None:
                    syncer_users.update_user_policy_info(conn, cache, times.get_current_timestamp(), for_user.policy_id)

                return Response(_lt.get_label(info.msg_notification_rule_deleted, lang))
            else:
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def get_user_advanced_permissions(request, conn=None):
    '''
    Get the advanced component permissions of a user.
    :param request: Http request
    :param conn: db connection
    :return: JSON response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.preferred_username]
        try:
            request_validator.validate_fields(request, expected_fields)
            conn = CONN_POOL.get_db_conn() if conn is None else conn

            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)
            pref_name = request.data[var_names.preferred_username]

            if not permissions.has_org_advanced_roles(org_perm):
                return Response(_lt.get_label(errors.err_subscription_rights, lang), status=403)

            adv_comp_perms = db_users.get_user_advanced_component_permissions(conn, times.get_current_timestamp(),
                                                                              pref_name)
            return Response(adv_comp_perms)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def add_user_advanced_permission(request, conn=None):
    '''
    Add an advanced component permission for a user.
    :param request: Http request
    :param conn: db connection
    :return: JSON response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.preferred_username, var_names.component_type,
                           var_names.reference_id, var_names.user_role]
        try:
            request_validator.validate_fields(request, expected_fields)
            conn = CONN_POOL.get_db_conn() if conn is None else conn

            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)
            if not permissions.has_org_advanced_roles(org_perm):
                return Response(_lt.get_label(errors.err_subscription_rights, lang), status=403)
            if not permissions.is_user_admin(user_perm):
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)

            pref_name = request.data[var_names.preferred_username]
            comp_type = request.data[var_names.component_type]
            ref_id = request.data[var_names.reference_id]
            user_role = request.data[var_names.user_role]
            current_time = times.get_current_timestamp()

            if comp_type == var_names.routines:
                comp_type_id = configs.routine_component_type_id
            elif comp_type == var_names.policies:
                comp_type_id = configs.policy_component_type_id
            elif comp_type == var_names.services:
                comp_type_id = configs.service_component_type_id
            elif comp_type == var_names.groups:
                if not permissions.has_org_permission(org_perm, permissions.ORG_GROUPS_PERMISSION):
                    return Response(_lt.get_label(errors.err_subscription_rights, lang), status=403)
                comp_type_id = configs.group_component_type_id
            else:
                raise SystemError(errors.err_internal_component_type_id_invalid)

            db_users.add_user_advanced_component_permission(conn, current_time, org_id, pref_name, comp_type_id,
                                                            ref_id, user_role)
            return Response(_lt.get_label(info.msg_success, lang))
        except LookupError as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def delete_user_advanced_permission(request, conn=None):
    '''
    Delete an advanced component permission for a user.
    :param request: Http request
    :param conn: db connection
    :return: JSON response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.preferred_username, var_names.component_type, var_names.reference_id]
        try:
            request_validator.validate_fields(request, expected_fields)
            conn = CONN_POOL.get_db_conn() if conn is None else conn

            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)
            if not permissions.has_org_advanced_roles(org_perm):
                return Response(_lt.get_label(errors.err_subscription_rights, lang), status=403)
            if not permissions.is_user_admin(user_perm):
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)

            pref_name = request.data[var_names.preferred_username]
            comp_type = request.data[var_names.component_type]
            ref_id = request.data[var_names.reference_id]
            current_time = times.get_current_timestamp()

            if comp_type == var_names.routines:
                comp_type_id = configs.routine_component_type_id
            elif comp_type == var_names.policies:
                comp_type_id = configs.policy_component_type_id
            elif comp_type == var_names.services:
                comp_type_id = configs.service_component_type_id
            elif comp_type == var_names.groups:
                if not permissions.has_org_permission(org_perm, permissions.ORG_GROUPS_PERMISSION):
                    return Response(_lt.get_label(errors.err_subscription_rights, lang), status=403)
                comp_type_id = configs.group_component_type_id
            else:
                raise SystemError(errors.err_internal_component_type_id_invalid)

            db_users.delete_user_advanced_component_permission(
                conn, current_time, org_id, pref_name, comp_type_id, ref_id)
            return Response(_lt.get_label(info.msg_success, lang))
        except LookupError as e:
            logging.exception(str(e))
            return Response(_lt.get_label(str(e), lang), status=400)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def fetch_labels(request):
    '''
    Get the labels for a given list of label names. This function
    is used to refresh the labels in the mobile app.
    :param request: Http request
    :return: JSON response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.language, var_names.labels]
        try:
            request_validator.validate_fields(request, expected_fields)
            tokenizer.authorize_request(request)

            new_labels = _lt.get_context(request.data[var_names.labels], request.data[var_names.language])
            return Response(new_labels)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)


@api_view(['POST'])
def get_user_timezone(request):
    '''
    Gets the timezone that the user is making the request from
    :param request: Http request
    :return: Http response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        try:
            ip_address = request.META.get(constants.ip_address_attribute)
            timezone = helpers.get_info_from_ip_address(ip_address, constants.ip_timezone_attribute)
            return Response(timezone)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)


@api_view(['POST'])
def has_url_file_access(request):
    '''
    Checks if a user has access to a url file.
    :param request: Http request
    :return: Http response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.url]
        try:
            request_validator.validate_fields(request, expected_fields)
            tokenizer.authorize_request(request)
            return Response(True)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.info(str(e))
            return Response(False, status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)


@api_view(['POST'])
def exists_onboarding_flow(request, conn=None):
    '''
    Checks if an onboarding flow exists or not.
    :param request: Http request
    :param conn: db connection
    :return: JSON response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.onboarding_flow, var_names.tutorial_name]
        try:
            request_validator.validate_fields(request, expected_fields)
            conn = CONN_POOL.get_db_conn() if conn is None else conn

            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)
            flow_id = request.data[var_names.onboarding_flow]
            ttr_name = request.data[var_names.tutorial_name]
            is_owner = permissions.is_user_owner(user_perm)

            exists = False
            if (ttr_name == tutorial_names.welcome and is_owner) or (ttr_name == tutorial_names.intro and not is_owner):
                exists = db_users.has_user_onboarding_flow(
                    conn, times.get_current_timestamp(), org_id, user_id, flow_id)

            return Response(exists)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def unsubscribe_user_from_onboarding_emails(request, conn=None):
    '''
    Ends a user's subscription for onboarding emails.
    :param request: Http request
    :param conn: db connection
    :return: JSON response
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.token]
        try:
            request_validator.validate_fields(request, expected_fields)
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            recipient_identifier = request.data[var_names.token]
            account_id, pref_name = key_manager.decode_general_identifier(recipient_identifier)
            user_id = db_users.get_user_id_from_account_id_and_preferred_username(
                conn, times.get_current_timestamp(), account_id, pref_name)
            if user_id is not None:
                db_users.end_onboarding_email_subscription(conn, times.get_current_timestamp(), user_id)
            return Response(_lt.get_label(info.msg_success, lang))
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def user_on_call_shifts(request, conn=None):
    '''
    Get a user's on-call calendar.
    :param request: Http request
    :param conn: db connection
    :return: Http response -> dict
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.preferred_username, var_names.start_date, var_names.end_date]
        optional_fields = [var_names.timezone]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            request_validator.validate_fields(request, expected_fields, optional_fields)
            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)

            pref_name = request.data[var_names.preferred_username]
            start_date = times.get_date_from_string(request.data[var_names.start_date]) - datetime.timedelta(days=1)
            end_date = times.get_date_from_string(request.data[var_names.end_date]) + datetime.timedelta(days=1)
            current_time = times.get_current_timestamp()

            if pref_name is None or\
                    permissions.has_user_permission(user_perm, permissions.USER_ORG_MEMBERS_VIEW_PERMISSION):

                if pref_name is None:
                    usr_obj = db_users.get_user(conn, current_time, user_id=user_id)[user_id]
                else:
                    usr_obj = db_users.get_user(
                        conn, current_time, keyed_on=var_names.preferred_username, preferred_username=pref_name
                    )[pref_name]
                historical_policy_data = db_policies.get_historical_policies(
                    conn, start_date, end_date, org_id, usr_obj.user_id)
                current_policy_data = db_policies.get_historical_policies(
                    conn, current_time, current_time, org_id, usr_obj.user_id)
                sel_tz = request.data[var_names.timezone] if var_names.timezone in request.data else usr_obj.timezone

                on_call_calendar = on_call_manager.prepare_on_call_calendar(
                    historical_policy_data, usr_obj.policy_id, sel_tz, start_date, end_date)
                period_on_call_shifts = on_call_manager.get_on_call_roles(
                    historical_policy_data, usr_obj.policy_id, sel_tz, current_time, configs.next_on_call_look_forward
                )[0]
                on_call_shifts, curr_on_call, next_on_call = on_call_manager.get_on_call_roles(
                    current_policy_data, usr_obj.policy_id, sel_tz, current_time, configs.next_on_call_look_forward
                )

                shift_keys = [x[var_names.key] for x in on_call_shifts]
                for item in period_on_call_shifts:
                    key = item[var_names.key]
                    if key not in shift_keys:
                        on_call_shifts.append(item)
                        shift_keys.append(key)

                details = {
                    var_names.display_name: usr_obj.first_name + ' ' + usr_obj.last_name,
                    var_names.preferred_username: usr_obj.preferred_username,
                    var_names.timezone: sel_tz,
                    var_names.data: on_call_calendar,
                    var_names.on_call_shifts: on_call_shifts,
                    var_names.on_call: curr_on_call,
                    var_names.next_on_call: next_on_call
                }
                return Response(details)
            else:
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)


@api_view(['POST'])
def user_webcal_info(request, conn=None):
    '''
    Get the information needed to form a user's WebCal URL. This will be sent to the web server to form the actual url.
    :param request: Http request
    :param conn: db connection
    :return: Http response -> dict
    '''
    if request.method == 'POST':
        lang = request_validator.get_user_language(request)
        expected_fields = [var_names.preferred_username]
        try:
            conn = CONN_POOL.get_db_conn() if conn is None else conn
            request_validator.validate_fields(request, expected_fields)
            user_id, org_id, user_perm, org_perm = tokenizer.authorize_request(request)

            pref_name = request.data[var_names.preferred_username]
            current_time = times.get_current_timestamp()

            if pref_name is None or\
                    permissions.has_user_permission(user_perm, permissions.USER_ORG_MEMBERS_VIEW_PERMISSION):

                subdomain, acc_id, usr_pref_name = db_accounts.get_org_info_for_webcal(conn, current_time, user_id)
                details = {
                    var_names.preferred_username: usr_pref_name if pref_name is None else pref_name,
                    var_names.subdomain: subdomain,
                    var_names.account_id: acc_id
                }
                return Response(details)
            else:
                return Response(_lt.get_label(errors.err_user_rights, lang), status=403)
        except LookupError as e:
            logging.info(str(e))
            return Response(_lt.get_label(errors.err_unknown_resource, lang), status=403)
        except (UnauthorizedRequest, jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_authorization, lang), status=401)
        except Exception as e:
            logging.exception(str(e))
            return Response(_lt.get_label(errors.err_system_error, lang), status=500)
        finally:
            CONN_POOL.put_db_conn(conn)
