def valid_name(name): """ Names must be between 2 and 50 characters. """ if not name: raise InvalidUsageError(message="Name is missing.") return 2 <= len(name) <= 50
def login(): """ Logs a user in by parsing a POST request containing user credentials. User provides email/password. Returns: errors if data is not valid or captcha fails. Returns: Access token and refresh token otherwise. """ r = request.get_json(force=True, silent=True) if not r: raise InvalidUsageError( message="Email and password must be included in the request body." ) email = r.get("email", None) password = r.get("password", None) recaptcha_token = r.get("recaptchaToken", None) if check_email(email): user = db.session.query(Users).filter_by(user_email=email).one_or_none() else: raise UnauthorizedError(message="Wrong email or password. Try again.") if not user or not password_valid(password) or not user.check_password(password): raise UnauthorizedError(message="Wrong email or password. Try again.") # Verify captcha with Google secret_key = os.environ.get("RECAPTCHA_SECRET_KEY") data = {"secret": secret_key, "response": recaptcha_token} resp = requests.post( "https://www.google.com/recaptcha/api/siteverify", data=data ).json() # Google will return True/False in the success field, resp must be json to properly access if not resp["success"]: raise UnauthorizedError(message="Captcha did not succeed.") access_token = create_access_token(identity=user, fresh=True) refresh_token = create_refresh_token(identity=user) response = make_response( jsonify( { "message": "successfully logged in user", "access_token": access_token, "user": { "first_name": user.first_name, "last_name": user.last_name, "email": user.user_email, "user_uuid": user.user_uuid, "quiz_id": user.quiz_uuid, }, } ), 200, ) response.set_cookie("refresh_token", refresh_token, path="/refresh", httponly=True) return response
def get_general_solutions(): """ The front-end needs general solutions list and information to serve to user when they click the general solutions menu button. General solutions are ordered based on relevance predicted from users personal values. """ quiz_uuid = request.args.get("quizId") user_scores = None if quiz_uuid: try: quiz_uuid = uuid.UUID(request.args.get("quizId")) user_scores = get_scores_vector(quiz_uuid) except: raise InvalidUsageError( message= "Malformed request. Quiz ID provided to get solutions is not a valid UUID." ) if user_scores == "Not in db": raise InvalidUsageError( message="Malformed request. Quid ID provided is not in database.") if user_scores: user_scores = [np.array(user_scores)] user_liberal, user_conservative = predict_radical_political( user_scores) else: user_liberal, user_conservative = None, None try: recommended_general_solutions = ( SOLUTION_PROCESSOR.get_user_general_solution_nodes( user_liberal, user_conservative)) climate_general_solutions = { "solutions": recommended_general_solutions } return jsonify(climate_general_solutions), 200 except: raise CustomError( message= "An error occurred while processing the user's general solution nodes." )
def post_code(): """ Accepts a quizId and postCode and updates the Scores object in the database to include the post code. """ try: request_body = request.json quiz_uuid = uuid.UUID(request_body["quizId"]) post_code = request_body["postCode"] except: raise InvalidUsageError( message="Unable to post postcode. Check the request parameters." ) if check_post_code(post_code): return store_post_code(post_code, quiz_uuid) else: raise InvalidUsageError(message="The postcode provided is not valid.")
def subscribe(): try: request_body = request.json email = request_body["email"] session_uuid = request.headers.get("X-Session-Id") except: raise InvalidUsageError( message= "Unable to post subscriber information. Check the request parameters." ) if not session_uuid: raise InvalidUsageError( message="Cannot post subscriber information without a session ID.") if check_email(email): return store_subscription_data(session_uuid, email) else: raise InvalidUsageError( message= "Cannot post subscriber information. Subscriber email is invalid.")
def get_feed(): """ The front-end needs to request personalized climate change effects that are most relevant to a user to display in the user's feed. PARAMETER (as GET) ------------------ session-id : uuid4 as string """ N_FEED_CARDS = 21 try: quiz_uuid = uuid.UUID(request.args.get("quizId")) except: raise InvalidUsageError( message= "Malformed request. Quiz ID provided to get feed is not a valid UUID." ) session_uuid = request.headers.get("X-Session-Id") if not session_uuid: raise InvalidUsageError( message="Cannot get feed without a session ID.") try: session_uuid = uuid.UUID(session_uuid) except: raise InvalidUsageError( message="Session ID used to get feed is not a valid UUID.") valid_session_uuid = Sessions.query.get(session_uuid) if valid_session_uuid: feed_entries = get_feed_results(quiz_uuid, N_FEED_CARDS, session_uuid) else: raise InvalidUsageError( message="Session ID used to get feed is not in the db.") return feed_entries
def password_valid(password): """ Passwords must contain at least one digit or special character. Passwords must be between 8 and 128 characters. Passwords cannot contain spaces. """ if not password: raise InvalidUsageError( message="Email and password must be included in the request body." ) conds = [ lambda s: any(x.isdigit() or not x.isalnum() for x in s), lambda s: all(not x.isspace() for x in s), lambda s: 8 <= len(s) <= 128, ] return all(cond(password) for cond in conds)
def store_subscription_data(session_uuid, email): email_in_db = Signup.query.filter_by(signup_email=email).first() try: valid_uuid = uuid.UUID(session_uuid) except: raise InvalidUsageError( message="Session ID used to sign up is not a valid UUID." ) valid_session_uuid = Sessions.query.get(session_uuid) if email_in_db: raise AlreadyExistsError(message="Subscriber email address") elif not valid_session_uuid: raise DatabaseError( message="Cannot save subscription information. Session ID not in the database." ) else: try: new_subscription = Signup() new_subscription.signup_email = email new_subscription.session_uuid = session_uuid now = datetime.datetime.now(timezone.utc) new_subscription.signup_timestamp = now db.session.add(new_subscription) db.session.commit() response = { "message": "Successfully added email", "email": email, "sessionId": session_uuid, "datetime": now, } return response, 201 except: raise DatabaseError( message="An error occurred while saving the subscription information to the database." )
def check_email(email): """ Checks an email format against the RFC 5322 specification. """ if not email: raise InvalidUsageError( message="Email and password must be included in the request body" ) if not isinstance(email, str): raise UnauthorizedError(message="Wrong email or password. Try again.") # RFC 5322 Specification as Regex regex = """(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\" (?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f]) *\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?: (?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1 [0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a \x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""" if re.search(regex, email): return True return False
def get_personal_values(): """ Users want to know their personal values based on their Schwartz questionnaire results. This returns the top 3 personal values with descriptions plus all scores for a user given a quiz ID. """ try: quiz_uuid = uuid.UUID(request.args.get("quizId")) except: raise InvalidUsageError( message= "Malformed request. Quiz ID provided to get personal values is not a valid UUID." ) scores = Scores.query.filter_by(quiz_uuid=quiz_uuid).first() if scores: personal_values_categories = [ "security", "conformity", "benevolence", "tradition", "universalism", "self_direction", "stimulation", "hedonism", "achievement", "power", ] scores = scores.__dict__ # All scores and accoiated values for response all_scores = [{ "personalValue": key, "score": scores[key] } for key in personal_values_categories] normalized_scores = normalize_scores(all_scores) # Top 3 personal values top_scores = sorted(all_scores, key=lambda value: value["score"], reverse=True)[:3] # Fetch descriptions try: file = os.path.join(os.getcwd(), "app/personal_values/static", "value_descriptions.json") with open(file) as f: value_descriptions = load(f) except FileNotFoundError: return jsonify({"error": "Value descriptions file not found"}), 404 # Add desciptions for top 3 values to retrun values_and_descriptions = [ value_descriptions[score["personalValue"]] for score in top_scores ] # Build and return response response = { "personalValues": values_and_descriptions, "valueScores": normalized_scores, } return jsonify(response), 200 else: raise DatabaseError( message="Cannot get personal values. Quiz ID is not in database.")
def user_scores(): """ User scores are used to determine which solutions are best to serve the user. Users also want to be able to see their score results after submitting the survey. This route checks for a POST request from the front-end containing a JSON object with the users scores. The user can answer 10 or 20 questions. If they answer 20, the scores are averaged between the 10 additional and 10 original questions to get 10 corresponding value scores. Then to get a centered score for each value, each score value is subtracted from the overall average of all 10 or 20 questions. A quiz ID is saved with the scores in the database. Returns: SessionID (UUID4) """ parameter = request.json if not parameter: raise InvalidUsageError( message="Cannot post scores. No user response provided." ) responses_to_add = 10 questions = parameter["questionResponses"] if len(questions["SetOne"]) != responses_to_add: raise InvalidUsageError( message="Cannot post scores. Invalid number of questions provided." ) process_scores = ProcessScores(questions) process_scores.calculate_scores("SetOne") if "SetTwo" in questions: process_scores.calculate_scores("SetTwo") process_scores.center_scores() value_scores = process_scores.get_value_scores() quiz_uuid = uuid.uuid4() value_scores["quiz_uuid"] = quiz_uuid user_uuid = None if current_user: user_uuid = current_user.user_uuid session_uuid = request.headers.get("X-Session-Id") if not session_uuid: raise InvalidUsageError(message="Cannot post scores without a session ID.") try: session_uuid = uuid.UUID(session_uuid) except: raise InvalidUsageError( message="Session ID used to post scores is not a valid UUID." ) valid_session_uuid = Sessions.query.get(session_uuid) if valid_session_uuid: process_scores.persist_scores(user_uuid, session_uuid) else: raise InvalidUsageError( message="Session ID used to save scores is not in the db." ) response = {"quizId": quiz_uuid} return jsonify(response), 201
def register(): """ Registration endpoint Takes a first name, last name, email, and password, validates this data and saves the user into the database. The user should automatically be logged in upon successful registration. The same email cannot be used for more than one account. Users will have to take the quiz before registering, meaning the quiz_uuid is linked to scores. Returns: Errors if any data is invalid Returns: Access Token and Refresh Token otherwise """ r = request.get_json(force=True, silent=True) if not r: raise InvalidUsageError( message="Email and password must be included in the request body." ) first_name = r.get("firstName", None) last_name = r.get("lastName", None) email = r.get("email", None) password = r.get("password", None) quiz_uuid = r.get("quizId", None) if not valid_name(first_name): raise InvalidUsageError( message="First name must be between 2 and 50 characters." ) if not valid_name(last_name): raise InvalidUsageError( message="Last name must be between 2 and 50 characters." ) if not quiz_uuid: raise InvalidUsageError(message="Quiz UUID must be included to register.") try: quiz_uuid = uuid.UUID(quiz_uuid) except: raise InvalidUsageError(message="Quiz UUID is improperly formatted.") if not scores_in_db(quiz_uuid): raise DatabaseError(message="Quiz ID is not in the db.") if not check_email(email): raise InvalidUsageError(message=f"The email {email} is invalid.") if not password_valid(password): raise InvalidUsageError( message="Password does not fit the requirements. " "Password must be between 8-128 characters, contain at least one number or special character, and cannot contain any spaces." ) user = Users.find_by_email(email) if user: raise UnauthorizedError(message="Email already registered") else: user = add_user_to_db(first_name, last_name, email, password, quiz_uuid) access_token = create_access_token(identity=user, fresh=True) refresh_token = create_refresh_token(identity=user) response = make_response( jsonify( { "message": "Successfully created user", "access_token": access_token, "user": { "first_name": user.first_name, "last_name": user.last_name, "email": user.user_email, "user_uuid": user.user_uuid, "quiz_id": user.quiz_uuid, }, } ), 201, ) response.set_cookie("refresh_token", refresh_token, path="/refresh", httponly=True) return response