diff --git a/.gitignore b/.gitignore index 97124cc975..4259c9c0c1 100755 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ platform/**/*.json frontend/public api/schema.json +api/api.current.graphql **/node_modules diff --git a/api/db.py b/api/db.py index 5570cf52b3..ff0c9d9dd8 100644 --- a/api/db.py +++ b/api/db.py @@ -9,10 +9,11 @@ DB_PORT = os.getenv("DB_PORT") DB_NAME = os.getenv("DB_NAME") - +# Add this in later branch -> pool_pre_ping=True, engine = create_engine( - f"postgresql+psycopg2://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}" + f"postgresql+psycopg2://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}", ) + db_session = scoped_session( sessionmaker(autocommit=False, autoflush=False, bind=engine) ) diff --git a/api/functions/auth_wrappers.py b/api/functions/auth_wrappers.py index eeb7e29ab1..ff399b9d40 100644 --- a/api/functions/auth_wrappers.py +++ b/api/functions/auth_wrappers.py @@ -54,7 +54,7 @@ def check_user_claims(user_id): "permission": select["permission"], } user_roles.append(temp_dict) - return user_roles + return user_roles def require_token(method): diff --git a/api/functions/verification_email.py b/api/functions/verification_email.py new file mode 100644 index 0000000000..22d9b8d3e4 --- /dev/null +++ b/api/functions/verification_email.py @@ -0,0 +1,59 @@ +import time + +from flask import request +from graphql import GraphQLError +from notifications_python_client.notifications import NotificationsAPIClient +from requests import HTTPError + +from json_web_token import tokenize +from models import Users + + +def send_verification_email(user: Users, client: NotificationsAPIClient): + """ + This function allows a user object to be passed in during account creation + and send an email to be used for verifying accounts + :param user: A instance of /models/User.py + :param client: An instance of NotificationsAPIClient + :return: None + """ + # Create Notify Client + notify_client = client + + # Check to see if users preferred lang is English or French + if user.preferred_lang == "french": + email_template_id = "f2c9b64a-c754-4ffd-93e9-33fdb0b5ae0b" + else: + email_template_id = "6e3368a7-0d75-47b1-b4b2-878234e554c9" + + # URL Generation + token = tokenize(user_id=user.user_name, exp_period=24) + url = str(request.url_root) + "validate/" + str(token) + + # Send Email + try: + response = notify_client.send_email_notification( + email_address=user.user_name, + template_id=email_template_id, + personalisation={"user": "", "verify_email_url": url}, + ) + + except HTTPError: + raise GraphQLError( + "Error, when sending verification email, error: {}".format(HTTPError) + ) + + # Sleep to wait and see if email was successful + time.sleep(1.5) + email_status = notify_client.get_notification_by_id(response.get("id")).get( + "status" + ) + + if ( + email_status == "permanent-failure" + or email_status == "temporary-failure" + or email_status == "technical-failure" + ): + return "Email Send Error: {}".format(email_status) + else: + return email_status diff --git a/api/json_web_token.py b/api/json_web_token.py index e9d8c8d063..64177b4e29 100644 --- a/api/json_web_token.py +++ b/api/json_web_token.py @@ -12,11 +12,12 @@ def tokenize( exp=None, # TODO: SUPER_SECRET_SALT isn't actually a salt! Give this a better name. secret=environ.get("SUPER_SECRET_SALT", ""), + exp_period=1, ): if not iat: iat = dt.timestamp(dt.utcnow()) if not exp: - exp = dt.timestamp(dt.utcnow() + timedelta(hours=1)) + exp = dt.timestamp(dt.utcnow() + timedelta(hours=exp_period)) return jwt.encode( {"exp": exp, "iat": iat, "user_id": user_id}, secret, algorithm="HS256", ).decode("utf-8") diff --git a/api/migrations/versions/2a25ac8483bc_.py b/api/migrations/versions/2a25ac8483bc_.py new file mode 100644 index 0000000000..e487913628 --- /dev/null +++ b/api/migrations/versions/2a25ac8483bc_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 2a25ac8483bc +Revises: c264f0905a36 +Create Date: 2020-05-20 13:38:34.078690 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "2a25ac8483bc" +down_revision = "c264f0905a36" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("users", sa.Column("email_validated", sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("users", "email_validated") + # ### end Alembic commands ### diff --git a/api/models/Users.py b/api/models/Users.py index ae0ee03ffd..4859984240 100644 --- a/api/models/Users.py +++ b/api/models/Users.py @@ -1,17 +1,15 @@ import bcrypt from sqlalchemy.types import Integer, Boolean, Float -from sqlalchemy.orm import relationship, validates -from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.orm import relationship from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method -from sqlalchemy import Column, String, ForeignKey -from sqlalchemy import event +from sqlalchemy import Column, String +from db import Base from functions.orm_to_dict import orm_to_dict from functions.slugify import slugify_value from models.Organizations import Organizations from models.User_affiliations import User_affiliations -from db import Base class Users(Base): @@ -28,18 +26,7 @@ class Users(Base): user_affiliation = relationship( "User_affiliations", back_populates="user", passive_deletes=True, ) - - def __init__(self, **kwargs): - super(Users, self).__init__(**kwargs) - # XXX: This is gross but matches the expections of the - # Acronym scalar type. - acronym = slugify_value(self.user_name).upper()[:50] - self.user_affiliation.append( - User_affiliations( - permission="admin", - user_organization=Organizations(name=self.user_name, acronym=acronym,), - ) - ) + email_validated = Column(Boolean, default=False) @hybrid_method def find_by_user_name(self, user_name): @@ -73,3 +60,17 @@ def password(self, password): self.user_password = bcrypt.hashpw( password.encode("utf8"), bcrypt.gensalt() ).decode("utf8") + + @hybrid_method + def verify_account(self): + # Set user email_validated field to true0 + self.email_validated = True + + # Create users sandbox org + acronym = slugify_value(self.user_name).upper()[:50] + self.user_affiliation.append( + User_affiliations( + permission="admin", + user_organization=Organizations(name=self.user_name, acronym=acronym,), + ) + ) diff --git a/api/queries.py b/api/queries.py index 1d7f2e9a4c..cfa3644a4a 100644 --- a/api/queries.py +++ b/api/queries.py @@ -65,6 +65,14 @@ # Request Scan Mutation from schemas.scans_mutation import RequestScan +# Verify Account Through Email +from schemas.email_verify_account.email_verify_account import EmailVerifyAccount + +# Re-send verification email +from schemas.send_email_verification.send_email_verification import ( + SendEmailVerification, +) + # Update User Role Mutation from schemas.user_affiliations import UpdateUserRole @@ -225,6 +233,12 @@ class Mutation(graphene.ObjectType): description="Allows users to give their credentials and be " "authenticated" ) sign_up = SignUp.Field(description="Allows users to sign up to our service") + email_verify_account = EmailVerifyAccount.Field( + description="Allows users to use token sent through email to verify their account." + ) + send_email_verification = SendEmailVerification.Field( + description="Allows users to resend verification if failed during sign-up" + ) schema = graphene.Schema(query=Query, mutation=Mutation) diff --git a/api/scalars/email_address.py b/api/scalars/email_address.py index aaf0ef0e65..b6077afefc 100644 --- a/api/scalars/email_address.py +++ b/api/scalars/email_address.py @@ -11,6 +11,10 @@ 0-9]|[01]?[0-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])+)\])""" +# GC_EMAIL_ADDRESS_REGEX = r"""(?:[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])?\.)+(gc|canada)\.(ca))""" + EMAIL_ADDRESS_REGEX = compile(EMAIL_ADDRESS_REGEX) diff --git a/api/schemas/User/user.py b/api/schemas/User/user.py index b489a04018..2cda6d7324 100644 --- a/api/schemas/User/user.py +++ b/api/schemas/User/user.py @@ -38,6 +38,7 @@ class Meta: "user_password", "password", "roles", + "email_validated" ) user_name = EmailAddress(description="Email that the user signed up with") @@ -46,6 +47,9 @@ class Meta: tfa = graphene.Boolean( description="Has the user completed two factor authentication" ) + email_validated = graphene.Boolean( + description="Has the user verified their account" + ) affiliations = graphene.ConnectionField( UserAffClass._meta.connection, description="Users access to organizations" ) @@ -64,6 +68,9 @@ def resolve_lang(self: UserModel, info): def resolve_tfa(self: UserModel, info): return self.tfa_validated + def resolve_email_validated(self: UserModel, info): + return self.email_validated + @require_token def resolve_affiliations(self: UserModel, info, **kwargs): user_roles = kwargs.get("user_roles") diff --git a/api/schemas/email_verify_account/__init__.py b/api/schemas/email_verify_account/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/schemas/email_verify_account/email_verify_account.py b/api/schemas/email_verify_account/email_verify_account.py new file mode 100644 index 0000000000..4714e9fd09 --- /dev/null +++ b/api/schemas/email_verify_account/email_verify_account.py @@ -0,0 +1,57 @@ +import graphene +import jwt +import os + +from graphql import GraphQLError + +from app import app +from db import db_session +from functions.input_validators import cleanse_input +from models.Users import Users + + +class EmailVerifyAccount(graphene.Mutation): + """ + Mutation that allows the user to verify their account through a token + sent in an email + """ + class Arguments: + token_string = graphene.String( + description="Token in sent via email, and located in url", required=True + ) + + status = graphene.Boolean( + description="Informs user if account was successfully verified." + ) + + @staticmethod + def mutate(self, info, **kwargs): + token_string = cleanse_input(kwargs.get("token_string")) + + try: + payload = jwt.decode( + token_string, os.getenv("SUPER_SECRET_SALT"), algorithms=["HS256"] + ) + except jwt.ExpiredSignatureError: + raise GraphQLError("Signature expired, please login again") + except jwt.InvalidTokenError: + raise GraphQLError("Invalid token, please login again") + user_name = payload.get("user_id") + + with app.app_context(): + # Check to see if user exists + user = db_session.query(Users).filter(Users.user_name == user_name).first() + + if not user: + raise GraphQLError("Error, User does not exist") + + user.verify_account() + + try: + db_session.commit() + except Exception as e: + db_session.rollback() + db_session.flush() + raise GraphQLError("Error, unable to verify account.") + + return EmailVerifyAccount(status=True) diff --git a/api/schemas/send_email_verification/__init__.py b/api/schemas/send_email_verification/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/schemas/send_email_verification/send_email_verification.py b/api/schemas/send_email_verification/send_email_verification.py new file mode 100644 index 0000000000..275102510d --- /dev/null +++ b/api/schemas/send_email_verification/send_email_verification.py @@ -0,0 +1,57 @@ +import graphene +import os + +from graphql import GraphQLError +from notifications_python_client.notifications import NotificationsAPIClient + +from db import db_session +from functions.input_validators import cleanse_input +from functions.verification_email import send_verification_email +from models.Users import Users +from scalars.email_address import EmailAddress + + +class SendEmailVerification(graphene.Mutation): + """ + This mutation is used for re-sending a verification email if it failed + during user creation + """ + + class Arguments: + user_name = EmailAddress( + description="The users email address used for sending email", required=True, + ) + + status = graphene.Boolean( + description="If email is successfully sent status will be true" + ) + + @staticmethod + def mutate(self, info, **kwargs): + # Get information from mutation arguments + user_name = cleanse_input(kwargs.get("user_name")) + + # Find user + user = db_session.query(Users).filter(Users.user_name == user_name).first() + + # Check to see if user is found, or if they are already validated + if user is None: + raise GraphQLError("Error, cannot find user.") + elif user.email_validated: + raise GraphQLError("Error, user is already validated.") + + # Send validation email + email_status = send_verification_email( + user=user, + client=NotificationsAPIClient( + api_key=os.getenv("NOTIFICATION_API_KEY"), + base_url=os.getenv("NOTIFICATION_API_URL"), + ) + ) + + if email_status.__contains__("Email Send Error"): + raise GraphQLError( + "Error, when sending verification email, please try again." + ) + + return SendEmailVerification(status=True) diff --git a/api/schemas/sign_up/create_user.py b/api/schemas/sign_up/create_user.py index 01b9981bbc..acf080c505 100644 --- a/api/schemas/sign_up/create_user.py +++ b/api/schemas/sign_up/create_user.py @@ -1,10 +1,15 @@ +import os + from graphql import GraphQLError +from requests import HTTPError +from notifications_python_client.notifications import NotificationsAPIClient from functions.input_validators import * from functions.error_messages import * from models import Users as User from db import db_session from json_web_token import tokenize +from functions.verification_email import send_verification_email def create_user(display_name, password, confirm_password, user_name, preferred_lang): @@ -41,14 +46,43 @@ def create_user(display_name, password, confirm_password, user_name, preferred_l password=password, ) db_session.add(user) + try: + # Add User to db db_session.commit() - auth_token = tokenize(user_id=user.id) + + email_response = send_verification_email( + user=user, + client=NotificationsAPIClient( + api_key=os.getenv("NOTIFICATION_API_KEY"), + base_url=os.getenv("NOTIFICATION_API_URL"), + ) + ) + + if email_response.__contains__("Email Send Error"): + raise GraphQLError( + "Error, when sending verification email, please try go to " + "user page to verify account" + ) + + # Get user id + user_id = ( + db_session.query(User).filter(User.user_name == user_name).first().id + ) + auth_token = tokenize(user_id=user_id) + return {"auth_token": auth_token, "user": user} + + except HTTPError: + raise GraphQLError( + "Error, when sending verification email, please try again" + ) + except Exception as e: db_session.rollback() db_session.flush() - raise GraphQLError(error_creating_account()) + # raise GraphQLError(error_creating_account()) + raise GraphQLError(str(e)) else: # Ensure that users have unique email addresses raise GraphQLError(error_email_in_use()) diff --git a/api/schemas/sign_up/sign_up.py b/api/schemas/sign_up/sign_up.py index 9ac4077c6f..c10132d583 100644 --- a/api/schemas/sign_up/sign_up.py +++ b/api/schemas/sign_up/sign_up.py @@ -31,8 +31,7 @@ class Arguments: required=True, ) preferred_lang = LanguageEnums( - description="Used to set users preferred language", - required=True, + description="Used to set users preferred language", required=True, ) # Define mutation fields diff --git a/api/schemas/user_page/user_page.py b/api/schemas/user_page/user_page.py index eaac5025bf..d536d1a806 100644 --- a/api/schemas/user_page/user_page.py +++ b/api/schemas/user_page/user_page.py @@ -29,6 +29,7 @@ class Meta: "user_affiliation", "password", "roles", + "email_validated" ) user_name = EmailAddress(description="The users email address or userName") @@ -37,6 +38,9 @@ class Meta: tfa = graphene.Boolean( description="Indicates wether or not this user has enabled tfa" ) + email_validated = graphene.Boolean( + description="Has the user verified their account" + ) user_affiliations = graphene.List( lambda: UserPageAffiliations, description="Indicates if this user is an admin of the organization " @@ -57,6 +61,9 @@ def resolve_lang(self: Users, info): def resolve_tfa(self: Users, info): return self.tfa_validated + def resolve_email_validated(self: Users, info): + return self.email_validated + @require_token def resolve_user_affiliations(self: Users, info, **kwargs): user_roles = kwargs.get("user_roles") diff --git a/api/tests/test_cost_check.py b/api/tests/test_cost_check.py index 4a7f28b148..f200992c79 100644 --- a/api/tests/test_cost_check.py +++ b/api/tests/test_cost_check.py @@ -20,7 +20,6 @@ def test_valid_cost_query(save): Test cost check function operates normally with valid query """ test_super_admin = Users( - id=2, display_name="testsuperadmin", user_name="testsuperadmin@testemail.ca", password="testpassword123", diff --git a/api/tests/test_language_enums.py b/api/tests/test_language_enums.py index 30b2378f33..ccee533573 100644 --- a/api/tests/test_language_enums.py +++ b/api/tests/test_language_enums.py @@ -11,4 +11,3 @@ def test_english_enum(): def test_french_enum(): assert LanguageEnums.FRENCH == "french" assert LanguageEnums.FRENCH == LanguageEnums.get("french") - diff --git a/api/tests/test_organization_resolver_access_control.py b/api/tests/test_organization_resolver_access_control.py index 55257afb60..ab140fdfd0 100644 --- a/api/tests/test_organization_resolver_access_control.py +++ b/api/tests/test_organization_resolver_access_control.py @@ -79,6 +79,8 @@ def test_org_resolvers_returns_all_orgs_to_super_admin(save): User_affiliations(permission="user_read", user_organization=org1) ], ) + reader.verify_account() + super_admin = Users( display_name="testsuperadmin", user_name="testsuperadmin@testemail.ca", @@ -87,6 +89,7 @@ def test_org_resolvers_returns_all_orgs_to_super_admin(save): User_affiliations(permission="super_admin", user_organization=org1) ], ) + super_admin.verify_account() save(reader) save(super_admin) @@ -136,6 +139,8 @@ def test_org_resolvers_returns_single_org1_and_users_own_org_for_read_users(save User_affiliations(permission="user_read", user_organization=org1) ], ) + reader.verify_account() + super_admin = Users( display_name="testsuperadmin", user_name="testsuperadmin@testemail.ca", @@ -145,6 +150,7 @@ def test_org_resolvers_returns_single_org1_and_users_own_org_for_read_users(save User_affiliations(permission="super_admin", user_organization=org2), ], ) + super_admin.verify_account() save(reader) save(super_admin) diff --git a/api/tests/test_organization_resolver_values.py b/api/tests/test_organization_resolver_values.py index b251331b37..10870df206 100644 --- a/api/tests/test_organization_resolver_values.py +++ b/api/tests/test_organization_resolver_values.py @@ -1,12 +1,6 @@ import pytest -import json from pytest import fail -from flask import Request -from graphene.test import Client -from unittest import TestCase -from werkzeug.test import create_environ -from app import app from db import DB from models import Organizations, Domains, Users, User_affiliations from tests.test_functions import json, run @@ -189,18 +183,21 @@ def test_get_org_resolvers_super_admin_multi_node(save): user_name="testuserread@testemail.ca", password="testpassword123", user_affiliation=[ - User_affiliations(user_organization=org1, permission="user_read") + User_affiliations(user_organization=org1, permission="user_read"), ], ) + user.verify_account() save(user) + super_admin = Users( display_name="testsuperadmin", user_name="testsuperadmin@testemail.ca", password="testpassword123", user_affiliation=[ - User_affiliations(user_organization=org1, permission="super_admin") + User_affiliations(user_organization=org1, permission="super_admin"), ], ) + super_admin.verify_account() save(super_admin) result = run( @@ -491,9 +488,10 @@ def test_get_org_resolvers_by_org_user_read_multi_node(save): user_name="testuserread@testemail.ca", password="testpassword123", user_affiliation=[ - User_affiliations(user_organization=org1, permission="user_read") + User_affiliations(user_organization=org1, permission="user_read"), ], ) + user.verify_account() save(user) result = run( diff --git a/api/tests/test_send_verification_email_function.py b/api/tests/test_send_verification_email_function.py new file mode 100644 index 0000000000..1ecc8eb69c --- /dev/null +++ b/api/tests/test_send_verification_email_function.py @@ -0,0 +1,58 @@ +import os + +from notifications_python_client import NotificationsAPIClient + +from app import app +from models.Users import Users +from functions.verification_email import send_verification_email + +NOTIFICATION_API_KEY = os.getenv("NOTIFICATION_API_KEY") +NOTIFICATION_API_URL = os.getenv("NOTIFICATION_API_URL") + + +def test_successful_send_verification_email(): + request_headers = {"Origin": "https://testserver.com"} + with app.test_request_context(headers=request_headers): + temp_user = Users( + user_name="successful.send.email@test.com", password="testpassword123", + ) + response = send_verification_email( + user=temp_user, + client=NotificationsAPIClient( + api_key=NOTIFICATION_API_KEY, + base_url=NOTIFICATION_API_URL, + ) + ) + assert response == "delivered" + + +def test_permanent_failure_send_verification_email(): + request_headers = {"Origin": "https://testserver.com"} + with app.test_request_context(headers=request_headers): + temp_user = Users( + user_name="perm-fail@simulator.notify", password="testpassword123", + ) + response = send_verification_email( + user=temp_user, + client=NotificationsAPIClient( + api_key=NOTIFICATION_API_KEY, + base_url=NOTIFICATION_API_URL, + ) + ) + assert response == "Email Send Error: permanent-failure" + + +def test_temporary_failure_send_verification_email(): + request_headers = {"Origin": "https://testserver.com"} + with app.test_request_context(headers=request_headers): + temp_user = Users( + user_name="temp-fail@simulator.notify", password="testpassword123", + ) + response = send_verification_email( + user=temp_user, + client=NotificationsAPIClient( + api_key=NOTIFICATION_API_KEY, + base_url=NOTIFICATION_API_URL, + ) + ) + assert response == "Email Send Error: temporary-failure" diff --git a/api/tests/test_send_verification_email_mutation.py b/api/tests/test_send_verification_email_mutation.py new file mode 100644 index 0000000000..2e9681272c --- /dev/null +++ b/api/tests/test_send_verification_email_mutation.py @@ -0,0 +1,119 @@ +import pytest + +from pytest import fail + +from app import app +from db import DB +from models.Users import Users +from tests.test_functions import json, run + + +@pytest.fixture() +def save(): + with app.app_context(): + s, cleanup, db_session = DB() + yield s + cleanup() + + +def test_successful_send_verification_email_mutation(save): + """ + Test to see if un-verified user can send verification email + """ + user = Users( + display_name="testuserread", + user_name="testuserread@testemail.ca", + password="testpassword123", + preferred_lang="English", + tfa_validated=False, + ) + save(user) + + request_headers = {"Origin": "https://testserver.com"} + with app.test_request_context(headers=request_headers): + result = run( + mutation=""" + mutation { + sendEmailVerification( + userName: "testuserread@testemail.ca" + ) { + status + } + } + """ + ) + + if "errors" in result: + fail("Expected to send verification email, instead: {}".format(json(result))) + + expected_result = {"data": {"sendEmailVerification": {"status": True}}} + + assert result == expected_result + + +def test_send_verification_email_mutation_user_already_verified(save): + """ + Test to see if user can't be found error message is sent + """ + user = Users( + display_name="testuserread", + user_name="testuserread@testemail.ca", + password="testpassword123", + preferred_lang="English", + tfa_validated=False, + ) + user.verify_account() + save(user) + + request_headers = {"Origin": "https://testserver.com"} + with app.test_request_context(headers=request_headers): + result = run( + mutation=""" + mutation { + sendEmailVerification( + userName: "testuserread@testemail.ca" + ) { + status + } + } + """ + ) + + if "errors" not in result: + fail( + "Expected to get and error that the user is already verified, instead: {}".format( + json(result) + ) + ) + + [error] = result["errors"] + assert error["message"] == "Error, user is already validated." + + +def test_send_verification_email_mutation_user_cant_be_found(save): + """ + Test to see if user can't be found error message is sent + """ + request_headers = {"Origin": "https://testserver.com"} + with app.test_request_context(headers=request_headers): + result = run( + mutation=""" + mutation { + sendEmailVerification( + userName: "testuserread@testemail.ca" + ) { + status + } + } + """ + ) + + if "errors" not in result: + fail( + "Expected to get and error that the user cant be found, instead: {}".format( + json(result) + ) + ) + + [error] = result["errors"] + assert error["message"] == "Error, cannot find user." diff --git a/api/tests/test_sign_up.py b/api/tests/test_sign_up.py index 3aadf9557e..3033ca72be 100644 --- a/api/tests/test_sign_up.py +++ b/api/tests/test_sign_up.py @@ -21,92 +21,96 @@ def test_successful_creation_english(save): """ Test that ensures a user can be created successfully using the api endpoint """ - result = run( - mutation=""" - mutation { - signUp( - displayName: "user-test" - userName: "different-email@testemail.ca" - password: "testpassword123" - confirmPassword: "testpassword123" - preferredLang: ENGLISH - ) { - authResult { - user { - userName - displayName - lang + request_headers = {"Origin": "https://testserver.com"} + with app.test_request_context(headers=request_headers): + result = run( + mutation=""" + mutation { + signUp( + displayName: "user-test" + userName: "different-email@testemail.ca" + password: "testpassword123" + confirmPassword: "testpassword123" + preferredLang: ENGLISH + ) { + authResult { + user { + userName + displayName + lang + } } } } - } - """, - ) + """, + ) - if "errors" in result: - fail("Tried to create a user, instead: {}".format(json(result))) - - expected_result = { - "data": { - "signUp": { - "authResult": { - "user": { - "userName": "different-email@testemail.ca", - "displayName": "user-test", - "lang": "english", + if "errors" in result: + fail("Tried to create a user, instead: {}".format(json(result))) + + expected_result = { + "data": { + "signUp": { + "authResult": { + "user": { + "userName": "different-email@testemail.ca", + "displayName": "user-test", + "lang": "english", + } } } } } - } - assert result == expected_result + assert result == expected_result def test_successful_creation_french(save): """ Test that ensures a user can be created successfully using the api endpoint """ - result = run( - mutation=""" - mutation { - signUp( - displayName: "user-test" - userName: "different-email@testemail.ca" - password: "testpassword123" - confirmPassword: "testpassword123" - preferredLang: FRENCH - ) { - authResult { - user { - userName - displayName - lang + request_headers = {"Origin": "https://testserver.com"} + with app.test_request_context(headers=request_headers): + result = run( + mutation=""" + mutation { + signUp( + displayName: "user-test" + userName: "different-email@testemail.ca" + password: "testpassword123" + confirmPassword: "testpassword123" + preferredLang: FRENCH + ) { + authResult { + user { + userName + displayName + lang + } } } } - } - """, - ) + """, + ) - if "errors" in result: - fail("Tried to create a user, instead: {}".format(json(result))) - - expected_result = { - "data": { - "signUp": { - "authResult": { - "user": { - "userName": "different-email@testemail.ca", - "displayName": "user-test", - "lang": "french", + if "errors" in result: + fail("Tried to create a user, instead: {}".format(json(result))) + + expected_result = { + "data": { + "signUp": { + "authResult": { + "user": { + "userName": "different-email@testemail.ca", + "displayName": "user-test", + "lang": "french", + } } } } } - } - assert result == expected_result + assert result == expected_result def test_email_address_in_use(save): diff --git a/api/tests/test_user_page_query.py b/api/tests/test_user_page_query.py index 68d07c3248..1b572b0cc1 100644 --- a/api/tests/test_user_page_query.py +++ b/api/tests/test_user_page_query.py @@ -57,6 +57,7 @@ def test_super_admin_can_see_other_user_in_different_org(save): ), ) ) + user_read.verify_account() save(user_read) result = run( @@ -88,8 +89,8 @@ def test_super_admin_can_see_other_user_in_different_org(save): "lang": "English", "tfa": False, "userAffiliations": [ - {"admin": True, "organization": "TESTUSERREAD-TESTEMAIL-CA"}, {"admin": False, "organization": "ORG1"}, + {"admin": True, "organization": "TESTUSERREAD-TESTEMAIL-CA"}, ], } } @@ -468,7 +469,7 @@ def test_user_read_cant_see_user_in_different_org(save): ) -def test_user_read_and_higher_can_own_information(save): +def test_user_read_can_see_own_information(save): """ Test to see that a user read can see their own information """ @@ -486,7 +487,7 @@ def test_user_read_and_higher_can_own_information(save): user_read.user_affiliation.append( User_affiliations(permission="user_read", user_organization=org_one,) ) - + user_read.verify_account() save(user_read) result = run( @@ -518,8 +519,8 @@ def test_user_read_and_higher_can_own_information(save): "lang": "English", "tfa": False, "userAffiliations": [ - {"admin": True, "organization": "TESTUSERREAD-TESTEMAIL-CA"}, {"admin": False, "organization": "ORG1"}, + {"admin": True, "organization": "TESTUSERREAD-TESTEMAIL-CA"}, ], } } diff --git a/frontend/schema.faker.graphql b/frontend/schema.faker.graphql index 445ddd9f28..c7fb61749a 100644 --- a/frontend/schema.faker.graphql +++ b/frontend/schema.faker.graphql @@ -398,6 +398,15 @@ type EmailScanEdge { cursor: String! } +""" +Mutation that allows the user to verify their account through a token +sent in an email +""" +type EmailVerifyAccount { + """Informs user if account was successfully verified.""" + status: Boolean +} + type HTTPS { """The ID of the object""" id: ID @@ -446,6 +455,14 @@ type Identifiers { ) } +enum LanguageEnums { + """Used for defining if English is the preferred language""" + ENGLISH + + """Used for defining if French is the preferred language""" + FRENCH +} + """The central gathering point for all of the GraphQL mutations.""" type Mutation { updatePassword(confirmPassword: String!, password: String!, userName: EmailAddress!): UpdateUserPassword @@ -559,12 +576,27 @@ type Mutation { """The password the user will authenticate with""" password: String! + """Used to set users preferred language""" + preferredLang: LanguageEnums! + """Email address that the user will use to authenticate with""" userName: EmailAddress! """Preferred language of the user""" preferredLang: LanguageEnums! ): SignUp + + """Allows users to use token sent through email to verify their account.""" + emailVerifyAccount( + """Token in sent via email, and located in url""" + tokenString: String! + ): EmailVerifyAccount + + """Allows users to resend verification if failed during sign-up""" + sendEmailVerification( + """The users email address used for sending email""" + userName: EmailAddress! + ): SendEmailVerification } """An object with an ID""" @@ -867,6 +899,14 @@ enum RoleEnums { enum LanguageEnums { ENGLISH FRENCH + +""" +This mutation is used for re-sending a verification email if it failed +during user creation +""" +type SendEmailVerification { + """If email is successfully sent status will be true""" + status: Boolean } """This method allows for new users to sign up for our sites services.""" @@ -1067,6 +1107,9 @@ type User implements Node { """Has the user completed two factor authentication""" tfa: Boolean @examples(values: [false, true]) + """Has the user verified their account""" + emailValidated: Boolean + """Users access to organizations""" affiliations(before: String, after: String, first: Int, last: Int): UserAffClassConnection } @@ -1156,6 +1199,9 @@ type UserPage implements Node { """Indicates wether or not this user has enabled tfa""" tfa: Boolean + """Has the user verified their account""" + emailValidated: Boolean + """Indicates if this user is an admin of the organization specified""" userAffiliations: [UserPageAffiliations] }