Skip to content

Commit 0026d04

Browse files
authored
Verify Your Account Today! (canada-ca#435)
* Added in initial version of gc email regex * added pool_pre_ping to db create engine * added default argument for exp time, and started work on email upon account creation * Implemented first iteration of verification emails * Added email_validated field to Users table * Create email verification mutation * Fix email sending email exception raising * add error check to see if user does exist * work on send_verification_email.py and its tests * Fixed checks in send_verification_email.py * Moved sandbox creation to when user verifies their account * Cleaned up imports * Created new hybrid method in Users.py that verifies the user's account * Updated all tests with new verify account which in turn creates the user sandbox * increase wait time * changed up user create, to create the user then attempt to send verification email, and made sleep time 1sec then check if email was successful * Add tests for successful, and temporary fail * Corrected verify account function to actually set field to true, and increased sleep time * Implemented new mutation for sending verification email, if failed in the account creation, and moved email_verification function into functions directory * Tests for new mutation * Black ran on files * Added api/api.current.graphql to gitignore, and updated description for EmailVerifyAccount mutation * Updated schema.faker.graphql, and added new email_verified field to user page, and user graphql objects * remove pre_pool_ping, and wait for new branch * Moved NotificationsAPI client to be a function argument
1 parent e16c059 commit 0026d04

26 files changed

Lines changed: 602 additions & 102 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
platform/**/*.json
44
frontend/public
55
api/schema.json
6+
api/api.current.graphql
67

78
**/node_modules
89

api/db.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
DB_PORT = os.getenv("DB_PORT")
1010
DB_NAME = os.getenv("DB_NAME")
1111

12-
12+
# Add this in later branch -> pool_pre_ping=True,
1313
engine = create_engine(
14-
f"postgresql+psycopg2://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
14+
f"postgresql+psycopg2://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}",
1515
)
16+
1617
db_session = scoped_session(
1718
sessionmaker(autocommit=False, autoflush=False, bind=engine)
1819
)

api/functions/auth_wrappers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def check_user_claims(user_id):
5454
"permission": select["permission"],
5555
}
5656
user_roles.append(temp_dict)
57-
return user_roles
57+
return user_roles
5858

5959

6060
def require_token(method):
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import time
2+
3+
from flask import request
4+
from graphql import GraphQLError
5+
from notifications_python_client.notifications import NotificationsAPIClient
6+
from requests import HTTPError
7+
8+
from json_web_token import tokenize
9+
from models import Users
10+
11+
12+
def send_verification_email(user: Users, client: NotificationsAPIClient):
13+
"""
14+
This function allows a user object to be passed in during account creation
15+
and send an email to be used for verifying accounts
16+
:param user: A instance of /models/User.py
17+
:param client: An instance of NotificationsAPIClient
18+
:return: None
19+
"""
20+
# Create Notify Client
21+
notify_client = client
22+
23+
# Check to see if users preferred lang is English or French
24+
if user.preferred_lang == "french":
25+
email_template_id = "f2c9b64a-c754-4ffd-93e9-33fdb0b5ae0b"
26+
else:
27+
email_template_id = "6e3368a7-0d75-47b1-b4b2-878234e554c9"
28+
29+
# URL Generation
30+
token = tokenize(user_id=user.user_name, exp_period=24)
31+
url = str(request.url_root) + "validate/" + str(token)
32+
33+
# Send Email
34+
try:
35+
response = notify_client.send_email_notification(
36+
email_address=user.user_name,
37+
template_id=email_template_id,
38+
personalisation={"user": "", "verify_email_url": url},
39+
)
40+
41+
except HTTPError:
42+
raise GraphQLError(
43+
"Error, when sending verification email, error: {}".format(HTTPError)
44+
)
45+
46+
# Sleep to wait and see if email was successful
47+
time.sleep(1.5)
48+
email_status = notify_client.get_notification_by_id(response.get("id")).get(
49+
"status"
50+
)
51+
52+
if (
53+
email_status == "permanent-failure"
54+
or email_status == "temporary-failure"
55+
or email_status == "technical-failure"
56+
):
57+
return "Email Send Error: {}".format(email_status)
58+
else:
59+
return email_status

api/json_web_token.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ def tokenize(
1212
exp=None,
1313
# TODO: SUPER_SECRET_SALT isn't actually a salt! Give this a better name.
1414
secret=environ.get("SUPER_SECRET_SALT", ""),
15+
exp_period=1,
1516
):
1617
if not iat:
1718
iat = dt.timestamp(dt.utcnow())
1819
if not exp:
19-
exp = dt.timestamp(dt.utcnow() + timedelta(hours=1))
20+
exp = dt.timestamp(dt.utcnow() + timedelta(hours=exp_period))
2021
return jwt.encode(
2122
{"exp": exp, "iat": iat, "user_id": user_id}, secret, algorithm="HS256",
2223
).decode("utf-8")
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""empty message
2+
3+
Revision ID: 2a25ac8483bc
4+
Revises: c264f0905a36
5+
Create Date: 2020-05-20 13:38:34.078690
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "2a25ac8483bc"
14+
down_revision = "c264f0905a36"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.add_column("users", sa.Column("email_validated", sa.Boolean(), nullable=True))
22+
# ### end Alembic commands ###
23+
24+
25+
def downgrade():
26+
# ### commands auto generated by Alembic - please adjust! ###
27+
op.drop_column("users", "email_validated")
28+
# ### end Alembic commands ###

api/models/Users.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import bcrypt
22

33
from sqlalchemy.types import Integer, Boolean, Float
4-
from sqlalchemy.orm import relationship, validates
5-
from sqlalchemy.ext.associationproxy import association_proxy
4+
from sqlalchemy.orm import relationship
65
from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
7-
from sqlalchemy import Column, String, ForeignKey
8-
from sqlalchemy import event
6+
from sqlalchemy import Column, String
97

8+
from db import Base
109
from functions.orm_to_dict import orm_to_dict
1110
from functions.slugify import slugify_value
1211
from models.Organizations import Organizations
1312
from models.User_affiliations import User_affiliations
14-
from db import Base
1513

1614

1715
class Users(Base):
@@ -28,18 +26,7 @@ class Users(Base):
2826
user_affiliation = relationship(
2927
"User_affiliations", back_populates="user", passive_deletes=True,
3028
)
31-
32-
def __init__(self, **kwargs):
33-
super(Users, self).__init__(**kwargs)
34-
# XXX: This is gross but matches the expections of the
35-
# Acronym scalar type.
36-
acronym = slugify_value(self.user_name).upper()[:50]
37-
self.user_affiliation.append(
38-
User_affiliations(
39-
permission="admin",
40-
user_organization=Organizations(name=self.user_name, acronym=acronym,),
41-
)
42-
)
29+
email_validated = Column(Boolean, default=False)
4330

4431
@hybrid_method
4532
def find_by_user_name(self, user_name):
@@ -73,3 +60,17 @@ def password(self, password):
7360
self.user_password = bcrypt.hashpw(
7461
password.encode("utf8"), bcrypt.gensalt()
7562
).decode("utf8")
63+
64+
@hybrid_method
65+
def verify_account(self):
66+
# Set user email_validated field to true0
67+
self.email_validated = True
68+
69+
# Create users sandbox org
70+
acronym = slugify_value(self.user_name).upper()[:50]
71+
self.user_affiliation.append(
72+
User_affiliations(
73+
permission="admin",
74+
user_organization=Organizations(name=self.user_name, acronym=acronym,),
75+
)
76+
)

api/queries.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@
6565
# Request Scan Mutation
6666
from schemas.scans_mutation import RequestScan
6767

68+
# Verify Account Through Email
69+
from schemas.email_verify_account.email_verify_account import EmailVerifyAccount
70+
71+
# Re-send verification email
72+
from schemas.send_email_verification.send_email_verification import (
73+
SendEmailVerification,
74+
)
75+
6876
# Update User Role Mutation
6977
from schemas.user_affiliations import UpdateUserRole
7078

@@ -225,6 +233,12 @@ class Mutation(graphene.ObjectType):
225233
description="Allows users to give their credentials and be " "authenticated"
226234
)
227235
sign_up = SignUp.Field(description="Allows users to sign up to our service")
236+
email_verify_account = EmailVerifyAccount.Field(
237+
description="Allows users to use token sent through email to verify their account."
238+
)
239+
send_email_verification = SendEmailVerification.Field(
240+
description="Allows users to resend verification if failed during sign-up"
241+
)
228242

229243

230244
schema = graphene.Schema(query=Query, mutation=Mutation)

api/scalars/email_address.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[
1212
\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"""
1313

14+
# GC_EMAIL_ADDRESS_REGEX = r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[
15+
# \x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[
16+
# a-z0-9])?\.)+(gc|canada)\.(ca))"""
17+
1418
EMAIL_ADDRESS_REGEX = compile(EMAIL_ADDRESS_REGEX)
1519

1620

api/schemas/User/user.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class Meta:
3838
"user_password",
3939
"password",
4040
"roles",
41+
"email_validated"
4142
)
4243

4344
user_name = EmailAddress(description="Email that the user signed up with")
@@ -46,6 +47,9 @@ class Meta:
4647
tfa = graphene.Boolean(
4748
description="Has the user completed two factor authentication"
4849
)
50+
email_validated = graphene.Boolean(
51+
description="Has the user verified their account"
52+
)
4953
affiliations = graphene.ConnectionField(
5054
UserAffClass._meta.connection, description="Users access to organizations"
5155
)
@@ -64,6 +68,9 @@ def resolve_lang(self: UserModel, info):
6468
def resolve_tfa(self: UserModel, info):
6569
return self.tfa_validated
6670

71+
def resolve_email_validated(self: UserModel, info):
72+
return self.email_validated
73+
6774
@require_token
6875
def resolve_affiliations(self: UserModel, info, **kwargs):
6976
user_roles = kwargs.get("user_roles")

0 commit comments

Comments
 (0)