Skip to content

Commit

Permalink
Working email verification
Browse files Browse the repository at this point in the history
  • Loading branch information
Liam Bates committed Mar 9, 2019
1 parent be88b20 commit 53bfd1f
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 79 deletions.
5 changes: 5 additions & 0 deletions auto_maint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect

from itsdangerous import URLSafeTimedSerializer

app = Flask(__name__)

Expand All @@ -28,8 +29,12 @@
# Set domain
app.config["SERVER_NAME"] = os.environ['SERVER_NAME']

# Set secret key
app.config['SECRET_KEY'] = os.environ['SECRET_KEY']

# Set timed serializer
ts = URLSafeTimedSerializer(app.config["SECRET_KEY"])

csrf = CSRFProtect(app)

import auto_maint.views
14 changes: 11 additions & 3 deletions auto_maint/forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
""" Forms used throughout the web app, using WTForms. """
from datetime import date

from flask import flash, Markup
from flask_wtf import FlaskForm
from werkzeug.security import check_password_hash
from wtforms import (BooleanField, HiddenField, PasswordField, StringField,
Expand Down Expand Up @@ -41,6 +42,14 @@ def pw_authenticate(form, field):
user.failed_login()
raise ValidationError()

elif not user.email_confirmed:
# Send the user another welcome / verification email
user.verification_email()

flash("""Email not yet confirmed. Please use the
verification link in the email provided. An additional email
with verification link has been sent to you.""", 'danger')

# Check if correct password
elif not check_password_hash(user.password_hash, field.data):
# Record a failed login
Expand All @@ -62,7 +71,6 @@ def logical_date(form, field):

def greater_odometer(form, field):
""" Ensure odometer reading provided is higher than most recent. """
print(form.vehicle.data)
# Ensure that new mileage is higher than previous
if field.data < form.vehicle.data.last_odometer().reading:
raise ValidationError(
Expand Down Expand Up @@ -272,7 +280,8 @@ class UpdateEmail(FlaskForm):
class UpdatePassword(FlaskForm):
email = HiddenField()
current_password = PasswordField(
'Current Password', [DataRequired(), pw_authenticate], description='Current Password')
'Current Password', [DataRequired(), pw_authenticate],
description='Current Password')
password = PasswordField(
'New Password',
validators=[
Expand All @@ -284,4 +293,3 @@ class UpdatePassword(FlaskForm):
confirm = PasswordField(
validators=[DataRequired()], description='Confirm New Password')
submit_password = SubmitField('Update Password')

43 changes: 2 additions & 41 deletions auto_maint/helpers.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
""" Helpful functions used in the auto_maint app. """
import datetime
import os
import smtplib
from email.message import EmailMessage
from functools import wraps

from flask import redirect, render_template, session

from auto_maint import app
from auto_maint.models import User
from flask import redirect, session


def login_required(f):
Expand All @@ -27,7 +22,7 @@ def decorated_function(*args, **kwargs):
return decorated_function


def email(message):
def send_email(message):
""" Sends the provided email message using SMTP. """
# Send message to the email server.
server = smtplib.SMTP(os.environ['SMTP_SERVER'])
Expand All @@ -37,40 +32,6 @@ def email(message):
server.quit()


def notify_users():
""" Routine script to send email notifications when a vehicle is overdue
maintenance. """
# Context to access DB from function
with app.app_context():
print("NOTIFY USERS RUNNING")
for user in User.query.all():
for user_vehicle in user.vehicles:
status = user_vehicle.status()
if 'Soon' in status or 'Overdue' in status:
if user_vehicle.last_notification:
days_since = datetime.datetime.today(
) - user_vehicle.last_notification
if days_since < datetime.timedelta(days=3):
continue

# Generate Email message to send
msg = EmailMessage()
msg['Subject'] = 'Your vehicle is due maintenance'
msg['From'] = '[email protected]'
msg['To'] = user.email

# Generate HTML for email
html = render_template(
'email/reminder.html', vehicle=user_vehicle)
msg.set_content(html, subtype='html')

# Send email
email(msg)

# Update DB to with timestamp
user_vehicle.notification_sent()


def standard_schedule(vehicle):
# Based on a 2001 Honda Accord
vehicle.add_maintenance(
Expand Down
27 changes: 25 additions & 2 deletions auto_maint/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
""" auto_maint app models defined """
import datetime
from email.message import EmailMessage

from flask import session
from flask import render_template, session, url_for

from auto_maint import db
from auto_maint import db, ts
from auto_maint.helpers import send_email


class User(db.Model):
Expand All @@ -15,6 +17,7 @@ class User(db.Model):
name = db.Column(db.String(64), nullable=False)
failed_logins = db.Column(db.SmallInteger, default=0, nullable=False)
blocked = db.Column(db.Boolean, default=False, nullable=False)
email_confirmed = db.Column(db.Boolean, default=False, nullable=False)
vehicles = db.relationship('Vehicle', cascade='all,delete', backref='user')

def __init__(self, email, password_hash, name):
Expand Down Expand Up @@ -45,6 +48,26 @@ def failed_login(self):
# Commit to db
db.session.commit()

def verification_email(self):
""" Send the user a welcome email with a verification link. """
# Generate Email message to send
msg = EmailMessage()
msg['Subject'] = 'Welcome to Auto Maintenance!'
msg['From'] = '[email protected]'
msg['To'] = self.email

# Generate email confirmation token and URL
token = ts.dumps(self.email, salt='email-confirm-key')
confirm_url = url_for('confirm_email', token=token, _external=True)

# Generate HTML for email
html = render_template(
'email/welcome.html', user=self, confirm_url=confirm_url)
msg.set_content(html, subtype='html')

# Send email
send_email(msg)

def delete(self):
""" Method to delete the current vehicle object from the DB. """
db.session.delete(self)
Expand Down
42 changes: 42 additions & 0 deletions auto_maint/scheduled_tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import datetime
from email.message import EmailMessage

from flask import render_template

from auto_maint import app
from auto_maint.helpers import send_email
from auto_maint.models import User


def notify_users():
""" Routine script to send email notifications when a vehicle is overdue
maintenance. """
# Context to access DB from function
with app.app_context():
print("NOTIFY USERS RUNNING")
for user in User.query.all():
for user_vehicle in user.vehicles:
status = user_vehicle.status()
if 'Soon' in status or 'Overdue' in status:
if user_vehicle.last_notification:
days_since = datetime.datetime.today(
) - user_vehicle.last_notification
if days_since < datetime.timedelta(days=3):
continue

# Generate Email message to send
msg = EmailMessage()
msg['Subject'] = 'Your vehicle is due maintenance'
msg['From'] = '[email protected]'
msg['To'] = user.email

# Generate HTML for email
html = render_template(
'email/reminder.html', vehicle=user_vehicle)
msg.set_content(html, subtype='html')

# Send email
send_email(msg)

# Update DB to with timestamp
user_vehicle.notification_sent()
7 changes: 5 additions & 2 deletions auto_maint/templates/email/welcome.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
<h1>Welcome!</h1>
<p>Hi {{ user.name }},</p>
<p>Welcome to Auto Maintenance.</p>
<p>We'll email you at this address to notify you of any upcoming maintenance on your vehicles.</p>
<p>To be begin to use the app, please add your vehicles on the app and your desired maintenance tasks.</p>
<p>To begin to use the app please click the link below to confirm your email acccount:</p>
<p><a href="{{ confirm_url }}">Confirm my Email Address</a></p>
<p>Once confirmed you will be able to begin to use the app to create maintenance schedule's for your vehicle and be
notified via email of any due tasks.</p>
<br>
<p>Thanks,</p>
<p>Auto Maintenance</p>
{% endblock %}
79 changes: 50 additions & 29 deletions auto_maint/views.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
""" Auto Maintenance views. Also features GET routes for the deletion of
objects. """
from email.message import EmailMessage

from flask import flash, jsonify, redirect, render_template, request, session
from werkzeug.security import generate_password_hash

from auto_maint import app, db
from auto_maint.forms import (
AddVehicleForm, EditMaintenanceForm, EditVehicleForm, LoginForm,
NewLogForm, NewMaintenanceForm, NewOdometerForm, RegistrationForm,
UpdateEmail, UpdateName, UpdatePassword)
from auto_maint.helpers import email, login_required, standard_schedule
from auto_maint import app, db, ts
from auto_maint.forms import (AddVehicleForm, EditMaintenanceForm,
EditVehicleForm, LoginForm, NewLogForm,
NewMaintenanceForm, NewOdometerForm,
RegistrationForm, UpdateEmail, UpdateName,
UpdatePassword)
from auto_maint.helpers import login_required, standard_schedule
from auto_maint.models import Log, Maintenance, Odometer, User, Vehicle


Expand Down Expand Up @@ -48,24 +48,16 @@ def index():
generate_password_hash(registration_form.password.data),
registration_form.name.data)
# Flash thank you message
flash('Account succesfully created.', 'success')
flash(
"""Account succesfully created. Please check your email and
confirm your email address using the link provided in the email.""",
'success')

# Start a new user session
session["user_id"] = user.user_id
# Save to DB
db.session.commit()

# Generate Email message to send
msg = EmailMessage()
msg['Subject'] = 'Welcome to Auto Maintenance!'
msg['From'] = '[email protected]'
msg['To'] = user.email

# Generate HTML for email
html = render_template('email/welcome.html', user=user)
msg.set_content(html, subtype='html')

# Send email
email(msg)
# Send the user a welcome / verification email
user.verification_email()

# Confirm to browser that all okay
return jsonify(status='ok')
Expand All @@ -79,6 +71,35 @@ def index():
'index.html', login_form=login_form, reg_form=registration_form)


@app.route('/confirm/<token>')
def confirm_email(token):
""" Takes the email confirmation token from the user and updates the DB to
reflect the confirmation. """
# Attempt to confirm confirmation token otherwise flash error
try:
user_email = ts.loads(token, salt="email-confirm-key", max_age=86400)
except:
flash('Unknown or outdated email confirmation.', 'danger')
return redirect('/')

# Find user in DB by email
user = User.query.filter(User.email == user_email).first()

# Set user's email confirmed value to true
user.email_confirmed = True

flash('Thank you for confirming your email address.', 'success')

# Log the user in
session["user_id"] = user.user_id

# Save to DB
db.session.commit()

# Send user to home page
return home()


@app.route('/home', methods=['POST', 'GET'])
@login_required
def home():
Expand All @@ -99,7 +120,7 @@ def home():
'success')
# Confirm to browser that all okay
return jsonify(status='ok')
# Query db for users info and vehicles
# Query DB for users info and vehicles
vehicles = Vehicle.query.filter(
Vehicle.user_id == session["user_id"]).all()
user = User.query.filter(User.user_id == session["user_id"]).first()
Expand All @@ -114,7 +135,7 @@ def vehicle(vehicle_id):
""" Provides an overview of a vehicle record and allows posting of new
odometer readings. """

# Pull vehicle from db using id
# Pull vehicle from DB using id
lookup_vehicle = Vehicle.query.filter(
Vehicle.vehicle_id == vehicle_id).first()

Expand Down Expand Up @@ -193,7 +214,7 @@ def vehicle(vehicle_id):
def delete_vehicle(vehicle_id):
""" Takes a URL and deletes the vehicle, by the ID provided. """

# Query the db for a matching vehicle
# Query the DB for a matching vehicle
del_vehicle = Vehicle.query.filter_by(vehicle_id=vehicle_id).first()

# Test whatever was returned to see if the vehicle is owned by the user
Expand All @@ -213,7 +234,7 @@ def delete_vehicle(vehicle_id):
def delete_odometer(reading_id):
""" Takes a URL and deletes the odometer, by the ID provided. """

# Query the db for a matching vehicle
# Query the DB for a matching vehicle
del_odom = Odometer.query.filter_by(reading_id=reading_id).first()

# Test whatever was returned to see if the vehicle is owned by the user
Expand All @@ -237,7 +258,7 @@ def delete_odometer(reading_id):
def maintenance(maintenance_id):
""" Shows a details of a particular scheduled maintenance event and allows
the user to create log entries for that task when performed. """
# Pull vehicle, maintenance and user reocrds from db using id
# Pull vehicle, maintenance and user reocrds from DB using id
lookup_maintenance = Maintenance.query.filter(
Maintenance.maintenance_id == maintenance_id).first()

Expand Down Expand Up @@ -293,7 +314,7 @@ def maintenance(maintenance_id):
def delete_maintenance(maintenance_id):
""" Takes a URL and deletes the vehicle, by the ID provided. """

# Query the db for a matching vehicle
# Query the DB for a matching vehicle
del_maintenance = Maintenance.query.filter_by(
maintenance_id=maintenance_id).first()

Expand All @@ -313,7 +334,7 @@ def delete_maintenance(maintenance_id):
def delete_log(log_id):
""" Takes a URL and deletes the log entry, by the ID provided. """

# Query the db for a matching vehicle
# Query the DB for a matching vehicle
del_log = Log.query.filter_by(log_id=log_id).first()

# Test whatever was returned to see if the vehicle is owned by the user
Expand Down
Loading

0 comments on commit 53bfd1f

Please sign in to comment.