Skip to content

Commit

Permalink
add email verification and password reset to flask backend
Browse files Browse the repository at this point in the history
  • Loading branch information
xochozomatli committed Jan 3, 2021
1 parent aa3dfe7 commit bc7c5fa
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 8 deletions.
12 changes: 11 additions & 1 deletion api/app/api/tokens.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from flask import jsonify, g, make_response, request
from flask import jsonify, g, make_response, request, redirect
from app import db
from app.models import User
from app.api import bp
Expand All @@ -17,6 +17,16 @@ def get_token():
# response.headers.add('Set-Cookie','refresh_token='+tokens_dict['refresh_token']+'; Expires='+timezone('GMT').localize(tokens_dict['refresh_token_expiration']).strftime('%a, %d-%b-%Y %H:%M:%S %Z')+'; SameSite=Lax; Path=/')
db.session.commit()
return response

@bp.route('/verify-email/<token>', methods=['GET'])
def verify_user_email(token):
user = User.verify_email(token)
g.current_user = user
if not user:
return error_response(404)
user.verified = True
db.session.commit()
return redirect(app.config['FRONTEND_URL'], code=303)

@bp.route('/refresh', methods=['GET'])
def refresh_token():
Expand Down
8 changes: 5 additions & 3 deletions api/app/api/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from app.api import bp
from app.api.auth import token_auth
from app.api.errors import error_response, bad_request
from app.email import send_verification_email
from datetime import datetime, timedelta

@bp.route('/users/current', methods=['GET'])
Expand Down Expand Up @@ -37,9 +38,10 @@ def create_user():
user.from_dict(data, new_user=True)
db.session.add(user)
db.session.commit()
token = user.get_token()
send_verification_email(user)
# token = user.get_token()
user_dict = user.to_dict()
user_dict['token'] = token
# user_dict['token'] = token
response = jsonify(user_dict)
response.status_code = 201
response.headers['Location'] = url_for('api.get_user', id=user.id)
Expand Down Expand Up @@ -77,4 +79,4 @@ def delete_user(id):
user = User.query.get_or_404(id)
db.session.delete(user)
db.session.commit()
return '', 204
return '', 204
40 changes: 40 additions & 0 deletions api/app/email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from threading import Thread
from flask import current_app, render_template
from flask_mail import Message
from app import mail

def send_async_email(app, msg):
with app.app_context():
mail.send(msg)

def send_email(subject, sender, recipients, text_body, html_body, attachments=None, sync=False):
msg = Message(subject, sender=sender, recipients=recipients)
msg.body = text_body
msg.html = html_body
if attachments:
for attachment in attachments:
msg.attach(*attachment)
if sync:
mail.send(msg)
else:
Thread(target=send_async_email, args=(current_app._get_current_object(), msg)).start()

def send_verification_email(user):
token = user.get_email_verification_token()
send_email('[Totlahtol] Verify Your Email',
sender=current_app.config['ADMINS'][0],
recipients=[user.email],
text_body=render_template('verify_email.txt',
user=user, token=token),
html_body=render_template('verify_email.html',
user=user, token=token))

def send_password_reset_email(user):
token = user.get_reset_password_token()
send_email('[Totlahtol] Reset Your Password',
sender=current_app.config['ADMINS'][0],
recipients=[user.email],
text_body=render_template('reset_password.txt',
user=user, token=token),
html_body=render_template('reset_password.html',
user=user, token=token))
33 changes: 31 additions & 2 deletions api/app/models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from flask import url_for
from flask import url_for, current_app
from datetime import datetime, timedelta
from time import time
from app import db
# from app.search import add_to_index, remove_from_index, query_index
from werkzeug.security import generate_password_hash, check_password_hash
from hashlib import md5
# import jwt
import jwt
import os
import base64
import threading
Expand Down Expand Up @@ -53,6 +53,7 @@ class User(PaginatedAPIMixin, db.Model):
#Basic User Info
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
verified = db.Column(db.Boolean, default=False)
password_hash = db.Column(db.String(128))
about_me = db.Column(db.String(140))
token = db.Column(db.String(32), index=True, unique=True)
Expand Down Expand Up @@ -122,11 +123,37 @@ def refresh(refresh_token):
print("Refresh Token Valid; Right this way, sir!")
return user.get_token()

def get_email_verification_token(self, expires_in=600):
return jwt.encode({'verify_user_email': self.id, 'exp': time() + expires_in},
current_app.config['SECRET_KEY'], algorithm='HS256')

def get_reset_password_token(self, expires_in=600):
return jwt.encode({'reset_password': self.id, 'exp': time() + expires_in},
current_app.config['SECRET_KEY'], algorithm='HS256')

@staticmethod
def verify_email(token):
try:
id = jwt.decode(token, current_app.config['SECRET_KEY'],
algorithms=['HS256'])['verify_user_email']
except:
return
return User.query.get(id)

@staticmethod
def verify_reset_password_token(token):
try:
id = jwt.decode(token, current_app.config['SECRET_KEY'],
algorithms=['HS256'])['reset_password']
except:
return
return User.query.get(id)

def to_dict(self, include_email=False):
data = {
'id': self.id,
'username': self.username,
'verified': str(self.verified).lower(),
# 'last_seen': self.last_seen.isoformat() + 'Z',
'about_me': self.about_me,
# 'post_count': self.posts.count(),
Expand Down Expand Up @@ -172,6 +199,8 @@ def get_rec(self):
# else returns recs based on item2item_similarity
pass



class Lesson(PaginatedAPIMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(120), index=True, unique=True)
Expand Down
12 changes: 12 additions & 0 deletions api/app/templates/reset_password.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<p>Dear {{ user.username }},</p>
<p>
To reset your password
<a href="{{ url_for('reset_password', token=token, _external=True) }}">
click here
</a>.
</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('reset_password', token=token, _external=True) }}</p>
<p>If you have not requested a password reset, simply ignore this message.</p>
<p>Sincerely,</p>
<p>The Totlahtol Team</p>
11 changes: 11 additions & 0 deletions api/app/templates/reset_password.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Dear {{ user.username }},

To reset your password click on the following link:

{{ url_for('api.reset_password', token=token, _external=True) }}

If you have not requested a password reset simply ignore this message.

Sincerely,

The Microblog Team
12 changes: 12 additions & 0 deletions api/app/templates/verify_email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<p>Dear {{ user.username }},</p>
<p>
To verify your email
<a href="{{ url_for('api.verify_user_email', token=token, _external=True) }}">
click here
</a>.
</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('api.verify_user_email', token=token, _external=True) }}</p>
<p>If you have not signed up for our app, simply ignore this message.</p>
<p>Sincerely,</p>
<p>The Totlahtol Team</p>
11 changes: 11 additions & 0 deletions api/app/templates/verify_email.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Dear {{ user.username }},

To verify your email click on the following link:

{{ url_for('api.verify_user_email', token=token, _external=True) }}

If you have not signed up for our app, simply ignore this message.

Sincerely,

The Microblog Team
3 changes: 2 additions & 1 deletion api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ class Config(object):
ADMINS = ['[email protected]']
# POSTS_PER_PAGE = 3
# LANGUAGES = ['en', 'es']
# MS_TRANSLATOR_KEY=os.environ.get('MS_TRANSLATOR_KEY')
# MS_TRANSLATOR_KEY=os.environ.get('MS_TRANSLATOR_KEY')
FRONTEND_URL = os.environ.get('TOTLAHTOL_FRONTEND_URL') or '/'
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""add 'verified' field to User model
Revision ID: 5171de0b0403
Revises: 5d988c97b3b7
Create Date: 2021-01-02 22:52:45.718436
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '5171de0b0403'
down_revision = '5d988c97b3b7'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('verified', sa.Boolean(), nullable=True))

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_column('verified')

# ### end Alembic commands ###
1 change: 1 addition & 0 deletions api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pytest-flask==1.1.0
python-dateutil==2.8.0
python-dotenv==0.10.2
python-editor==1.0.4
PyJWT==2.0.0
pytz==2019.1
regex==2020.11.13
requests==2.22.0
Expand Down
3 changes: 2 additions & 1 deletion api/totlahtol.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from app import create_app, db
from app import create_app, db#,cli
from app.models import User, Lesson, Tlahtolli, Review

app = create_app()
# cli.register(app)

@app.shell_context_processor
def make_shell_context():
Expand Down

0 comments on commit bc7c5fa

Please sign in to comment.