def change_respondent(payload, session): """ Modify an existing respondent's email address, identified by their current email address. """ v = Validator(Exists('email_address', 'new_email_address')) if not v.validate(payload): logger.info("Payload for change respondent was invalid", errors=v.errors) raise BadRequest(v.errors, 400) email_address = payload['email_address'] new_email_address = payload['new_email_address'] respondent = query_respondent_by_email(email_address, session) if not respondent: logger.info("Respondent does not exist") raise NotFound("Respondent does not exist") if new_email_address == email_address: return respondent.to_respondent_dict() respondent_with_new_email = query_respondent_by_email( new_email_address, session) if respondent_with_new_email: logger.info("Respondent with email already exists") raise Conflict("New email address already taken") respondent.pending_email_address = new_email_address # check if respondent has initiated this request if 'change_requested_by_respondent' in payload: verification_url = PublicWebsite().confirm_account_email_change_url( new_email_address) personalisation = { 'CONFIRM_EMAIL_URL': verification_url, 'FIRST_NAME': respondent.first_name } logger.info('Account change email URL for party_id', party_id=str(respondent.party_uuid), url=verification_url) _send_account_email_change_email( personalisation, template='verify_account_email_change', email=new_email_address, party_id=respondent.party_uuid) else: _send_email_verification(respondent.party_uuid, new_email_address) logger.info('Verification email sent for changing respondents email', respondent_id=str(respondent.party_uuid)) return respondent.to_respondent_dict()
def change_respondent_password(payload, tran, session): _is_valid(payload, attribute='new_password') respondent = query_respondent_by_email(payload['email_address'], session) email_address = respondent.email_address if not respondent: logger.info("Respondent does not exist") raise NotFound("Respondent does not exist") new_password = payload['new_password'] # Check and see if the account is active, if not we can now set to active if respondent.status != RespondentStatus.ACTIVE: # Checking enrolment status, if PENDING we will change it to ENABLED logger.info('Checking enrolment status', respondent_id=respondent.party_uuid) if respondent.pending_enrolment: enrol_respondent_for_survey(respondent, session) # We set the party as ACTIVE in this service respondent.status = RespondentStatus.ACTIVE oauth_response = OauthClient().update_account(username=email_address, password=new_password, account_locked='False') else: oauth_response = OauthClient().update_account(username=email_address, password=new_password) if oauth_response.status_code != 201: logger.error( "Unexpected response from auth service, unable to change user password", respondent_id=str(respondent.party_uuid), status=oauth_response.status_code) raise InternalServerError("Failed to change respondent password") personalisation = {'FIRST_NAME': respondent.first_name} party_id = respondent.party_uuid try: NotifyGateway(current_app.config).request_to_notify( email=email_address, template_name='confirm_password_change', personalisation=personalisation, reference=str(party_id)) except RasNotifyError as ras_error: logger.error(ras_error) # This ensures the log message is only written once the DB transaction is committed tran.on_success(lambda: logger.info( 'Respondent has changed their password', respondent_id=party_id)) return {'response': "Ok"}
def get_respondent_by_email(email, session): """ Get a verified respondent by its email address. Returns either the unique respondent identified by the supplied email address, or otherwise raises a RasError to indicate the email address doesn't exist. :param email: Email of respondent to lookup :rtype: Respondent """ respondent = query_respondent_by_email(email, session) if not respondent: logger.info("Respondent does not exist") raise NotFound("Respondent does not exist") return respondent.to_respondent_dict()
def verify_token(token, session): try: duration = current_app.config["EMAIL_TOKEN_EXPIRY"] email_address = decode_email_token(token, duration) except SignatureExpired: logger.info("Expired email verification token") raise Conflict("Expired email verification token") except (BadSignature, BadData): logger.exception("Bad token in verify_token") raise NotFound("Unknown email verification token") respondent = query_respondent_by_email(email_address, session) if not respondent: logger.info("Respondent with Email from token does not exist") raise NotFound("Respondent does not exist") return {'response': "Ok"}
def resend_password_email_expired_token(token, session): """ Check and resend an email verification email using the expired token :param token: the expired token :param session: database session :return: response """ email_address = decode_email_token(token) respondent = query_respondent_by_email(email_address, session) if not respondent: logger.info("Respondent does not exist") raise NotFound("Respondent does not exist") payload = {'email_address': email_address} response = request_password_change(payload) return response
def resend_verification_email_expired_token(token, session): """ Check and resend an email verification email using the expired token :param token: the expired token :param session: database session :return: response """ logger.info('Attempting to resend verification email with expired token', token=token) email_address = decode_email_token(token) respondent = query_respondent_by_email(email_address, session) if not respondent: logger.info("Respondent does not exist", token=token) raise NotFound("Respondent does not exist") response = _resend_verification_email(respondent) logger.info('Successfully resent verification email with expired token', token=token) return response
def update_respondent_mark_for_deletion(email, session): """ Update respondent flag mark_for_deletion :param email: email of Respondent to be marked for deletion :type email: str :param session: :return: On Success it returns None, on failure will raise exceptions """ respondent = query_respondent_by_email(email, session) if respondent: try: session.query(Respondent).filter(Respondent.party_uuid == respondent.party_uuid) \ .update({Respondent.mark_for_deletion: True}) return 'respondent successfully marked for deletion', 202 except (SQLAlchemyError, Exception) as error: logger.error('error with update respondent mark for deletion', error) return 'something went wrong', 500 else: return 'respondent does not exist', 404
def request_password_change(payload, session): _is_valid(payload, attribute='email_address') logger.info("Verifying user exists before sending password reset email") respondent = query_respondent_by_email(payload['email_address'], session) if not respondent: logger.info("Respondent does not exist") raise NotFound("Respondent does not exist") logger.info("Requesting password change", party_id=respondent.party_uuid) email_address = respondent.email_address verification_url = PublicWebsite().reset_password_url(email_address) personalisation = { 'RESET_PASSWORD_URL': verification_url, 'FIRST_NAME': respondent.first_name } party_id = str(respondent.party_uuid) logger.info('Reset password url', url=verification_url, party_id=party_id) try: NotifyGateway(current_app.config).request_to_notify( email=email_address, template_name='request_password_change', personalisation=personalisation, reference=party_id) except RasNotifyError: # Note: intentionally suppresses exception logger.error('Error sending request to Notify Gateway', respondent_id=party_id) logger.info('Password reset email successfully sent', party_id=party_id) return {'response': "Ok"}
def put_email_verification(token, tran, session): """ Verify email address, this method can be reached when registering or updating email address :param token: :param tran: :param session: db session :return: Verified respondent details """ logger.info('Attempting to verify email', token=token) try: duration = current_app.config["EMAIL_TOKEN_EXPIRY"] email_address = decode_email_token(token, duration) except SignatureExpired: logger.info("Expired email verification token") raise Conflict("Expired email verification token") except (BadSignature, BadData): logger.exception("Bad token in put_email_verification") raise NotFound("Unknown email verification token") respondent = query_respondent_by_email(email_address, session) if not respondent: logger.info("Attempting to find respondent by pending email address") # When changing contact details, unverified new email is in pending_email_address respondent = query_respondent_by_pending_email(email_address, session) if respondent: # Get old email address old_email_address = respondent.email_address update_verified_email_address(respondent, tran) # send confirmation email to old email address personalisation = { 'FIRST_NAME': respondent.first_name, 'NEW_EMAIL': respondent.email_address } logger.info( "Sending change of email on account to previously held email address" ) _send_account_email_change_email( personalisation=personalisation, template='confirm_change_to_account_email', email=old_email_address, party_id=respondent.party_uuid) else: logger.info("Unable to find respondent by pending email") raise NotFound( "Unable to find user while checking email verification token") if respondent.status != RespondentStatus.ACTIVE: # We set the party as ACTIVE in this service respondent.status = RespondentStatus.ACTIVE # Next we check if this respondent has a pending enrolment (there WILL be only one, set during registration) if respondent.pending_enrolment: enrol_respondent_for_survey(respondent, session) else: logger.info( 'No pending enrolment for respondent while checking email verification token', party_uuid=str(respondent.party_uuid)) # We set the user as verified on the OAuth2 server. set_user_verified(email_address) return respondent.to_respondent_dict()
def post_respondent(party, session): """ Register respondent and set up pending enrolment before account verification :param party: respondent to be created details :param session :return: created respondent """ # Validation, curation and checks expected = ('emailAddress', 'firstName', 'lastName', 'password', 'telephone', 'enrolmentCode') v = Validator(Exists(*expected)) if 'id' in party: # Note: there's not strictly a requirement to be able to pass in a UUID, this is currently supported to # aid with testing. logger.info("'id' in respondent post message") try: uuid.UUID(party['id']) except ValueError: logger.info("Invalid respondent id type", respondent_id=party['id']) raise BadRequest( f"'{party['id']}' is not a valid UUID format for property 'id'" ) if not v.validate(party): logger.debug(v.errors) raise BadRequest(v.errors) iac = request_iac(party['enrolmentCode']) if not iac.get('active'): logger.info("Inactive enrolment code") raise BadRequest("Enrolment code is not active") existing = query_respondent_by_email(party['emailAddress'].lower(), session) if existing: logger.info("Email already exists", party_uuid=str(existing.party_uuid)) raise BadRequest("Email address already exists") case_context = request_case(party['enrolmentCode']) case_id = case_context['id'] business_id = case_context['partyId'] collection_exercise_id = case_context['caseGroup']['collectionExerciseId'] collection_exercise = request_collection_exercise(collection_exercise_id) survey_id = collection_exercise['surveyId'] business = query_business_by_party_uuid(business_id, session) if not business: logger.error( "Could not locate business when creating business association", business_id=business_id, case_id=case_id, collection_exercise_id=collection_exercise_id) raise InternalServerError( "Could not locate business when creating business association") # Chain of enrolment processes translated_party = { 'party_uuid': party.get('id') or str(uuid.uuid4()), 'email_address': party['emailAddress'].lower(), 'first_name': party['firstName'], 'last_name': party['lastName'], 'telephone': party['telephone'], 'status': RespondentStatus.CREATED } # This might look odd but it's done in the interest of keeping the code working in the same way. # If raise_for_status in the function raises an error, it would've been caught by @with_db_session, # rolled back the db and raised it. Whether that's something we want is another question. try: respondent = _add_enrolment_and_auth(business, business_id, case_id, party, session, survey_id, translated_party) except HTTPError: logger.error("add_enrolment_and_auth raised an HTTPError", exc_info=True) session.rollback() raise # If the disabling of the enrolment code fails we log an error and continue anyway. In the interest of keeping # the code working in the same way (which may itself be wrong...) we'll handle the ValueError that can be raised # in the same way as before (rollback the session and raise) but it's not clear whether this is the desired # behaviour. try: disable_iac(party['enrolmentCode'], case_id) except ValueError: logger.error("disable_iac didn't return json in its response", exc_info=True) session.rollback() raise _send_email_verification(respondent.party_uuid, party['emailAddress'].lower()) return respondent.to_respondent_dict()