Working tests, hopefully all code changes done.

This commit is contained in:
Nicholas Staples
2016-01-27 12:22:32 +00:00
parent ce81a6f540
commit 6959d695d3
57 changed files with 1143 additions and 1578 deletions

View File

@@ -1,11 +0,0 @@
from app import db
from app.models import Roles
def insert_role(role):
db.session.add(role)
db.session.commit()
def get_role_by_id(id):
return Roles.query.filter_by(id=id).first()

View File

@@ -2,8 +2,7 @@ from datetime import datetime
from sqlalchemy.orm import load_only
from app import db, login_manager
from app.models import User
from app import login_manager
from app.main.encryption import hashpw
from app import user_api_client
@@ -14,12 +13,6 @@ def load_user(user_id):
return get_user_by_id(user_id)
def insert_user(user):
user.password = hashpw(user.password)
db.session.add(user)
db.session.commit()
# TODO Would be better to have a generic get and update for user
# something that replicates the sql functionality.
def get_user_by_id(id):
@@ -39,7 +32,7 @@ def verify_password(user, password):
def update_user(user):
return user_api_client.update_user(user)
return user_api_client.update_user(user)
def increment_failed_login_count(id):
@@ -55,27 +48,37 @@ def activate_user(user):
def update_email_address(id, email_address):
user = get_user_by_id(id)
user.email_address = email_address
db.session.add(user)
db.session.commit()
# TODO update user
def is_email_unique(email_address):
if user_api_client.get_user_by_email(email_address):
return False
return True
def update_mobile_number(id, mobile_number):
user = get_user_by_id(id)
user.mobile_number = mobile_number
db.session.add(user)
db.session.commit()
# TODO update user
def update_password(user, password):
user.password = hashpw(password)
user.password_changed_at = datetime.now()
user.state = 'active'
db.session.add(user)
db.session.commit()
# TODO update user
def request_password_reset(email):
user = get_user_by_email(email)
user.state = 'request_password_reset'
db.session.add(user)
db.session.commit()
# TODO update user
def send_verify_code(user_id, code_type):
return user_api_client.send_verify_code(user_id, code_type)
def check_verify_code(user_id, code, code_type):
return user_api_client.check_verify_code(user_id, code, code_type)

View File

@@ -1,55 +0,0 @@
from datetime import datetime, timedelta
from app import db
from app.main.encryption import hashpw
from app.models import VerifyCodes
def add_code(user_id, code, code_type):
code = VerifyCodes(user_id=user_id,
code=hashpw(code),
code_type=code_type,
expiry_datetime=datetime.now() + timedelta(hours=1))
db.session.add(code)
db.session.commit()
return code
def get_codes(user_id, code_type=None):
if not code_type:
return VerifyCodes.query.filter_by(user_id=user_id, code_used=False).all()
return VerifyCodes.query.filter_by(user_id=user_id, code_type=code_type, code_used=False).all()
def get_code_by_code(user_id, code, code_type):
return VerifyCodes.query.filter_by(user_id=user_id, code=hashpw(code), code_type=code_type).first()
def use_code(id):
verify_code = VerifyCodes.query.get(id)
verify_code.code_used = True
db.session.add(verify_code)
db.session.commit()
def use_code_for_user_and_type(user_id, code_type):
codes = VerifyCodes.query.filter_by(user_id=user_id, code_type=code_type, code_used=False).all()
for verify_code in codes:
verify_code.code_used = True
db.session.add(verify_code)
db.session.commit()
def get_code_by_id(id):
return VerifyCodes.query.get(id)
def add_code_with_expiry(user_id, code, code_type, expiry):
code = VerifyCodes(user_id=user_id,
code=hashpw(code),
code_type=code_type,
expiry_datetime=expiry)
db.session.add(code)
db.session.commit()

View File

@@ -11,8 +11,7 @@ from wtforms import (
)
from wtforms.validators import DataRequired, Email, Length, Regexp
from app.main.validators import Blacklist, ValidateUserCodes, CsvFileValidator
from app.main.dao import verify_codes_dao
from app.main.validators import Blacklist, CsvFileValidator
from app.main.encryption import check_hash
@@ -84,16 +83,14 @@ def sms_code():
return StringField('Text message confirmation code',
validators=[DataRequired(message='Text message confirmation code can not be empty'),
Regexp(regex=verify_code,
message='Text message confirmation code must be 5 digits'),
ValidateUserCodes(code_type='sms')])
message='Text message confirmation code must be 5 digits')])
def email_code():
verify_code = '^\d{5}$'
return StringField("Email confirmation code",
validators=[DataRequired(message='Email confirmation code can not be empty'),
Regexp(regex=verify_code, message='Email confirmation code must be 5 digits'),
ValidateUserCodes(code_type='email')])
Regexp(regex=verify_code, message='Email confirmation code must be 5 digits')])
class LoginForm(Form):
@@ -125,31 +122,45 @@ class RegisterUserForm(Form):
class TwoFactorForm(Form):
def __init__(self, user_codes, *args, **kwargs):
def __init__(self, validate_code_func, *args, **kwargs):
'''
Keyword arguments:
user_codes -- List of user code objects which have the fields
(code_type, expiry_datetime, code)
validate_code_func -- Validates the code with the API.
'''
self.user_codes = user_codes
self.validate_code_func = validate_code_func
super(TwoFactorForm, self).__init__(*args, **kwargs)
sms_code = sms_code()
def validate_sms_code(self, field):
is_valid, reason = self.validate_code_func(field.data)
if not is_valid:
raise ValidationError(reason)
class VerifyForm(Form):
def __init__(self, user_codes, *args, **kwargs):
def __init__(self, validate_code_func, *args, **kwargs):
'''
Keyword arguments:
user_codes -- List of user code objects which have the fields
(code_type, expiry_datetime, code)
validate_code_func -- Validates the code with the API.
'''
self.user_codes = user_codes
self.validate_code_func = validate_code_func
super(VerifyForm, self).__init__(*args, **kwargs)
sms_code = sms_code()
email_code = email_code()
def _validate_code(self, cde, code_type):
is_valid, reason = self.validate_code_func(cde, code_type)
if not is_valid:
raise ValidationError(reason)
def validate_email_code(self, field):
self._validate_code(field.data, 'email')
def validate_sms_code(self, field):
self._validate_code(field.data, 'sms')
class EmailNotReceivedForm(Form):
email_address = email_address()
@@ -218,9 +229,18 @@ class NewPasswordForm(Form):
class ChangePasswordForm(Form):
def __init__(self, validate_password_func, *args, **kwargs):
self.validate_password_func = validate_password_func
super(ChangePasswordForm, self).__init__(*args, **kwargs)
old_password = password('Current password')
new_password = password('New password')
def validate_old_password(self, field):
if not self.validate_password_func(field.data):
raise ValidationError('Invalid password')
class CsvUploadForm(Form):
file = FileField('File to upload', validators=[DataRequired(
@@ -232,20 +252,50 @@ class ChangeNameForm(Form):
class ChangeEmailForm(Form):
def __init__(self, validate_email_func, *args, **kwargs):
self.validate_email_func = validate_email_func
super(ChangeEmailForm, self).__init__(*args, **kwargs)
email_address = email_address()
def validate_email_address(self, field):
is_valid = self.validate_email_func(field.data)
if not is_valid:
raise ValidationError("The email address is already in use")
class ConfirmEmailForm(Form):
def __init__(self, validate_code_func, *args, **kwargs):
self.validate_code_func = validate_code_func
super(ConfirmEmailForm, self).__init__(*args, **kwargs)
email_code = email_code()
def validate_email_code(self, field):
is_valid, msg = self.validate_code_func(field.data)
if not is_valid:
raise ValidationError(msg)
class ChangeMobileNumberForm(Form):
mobile_number = mobile_number()
class ConfirmMobileNumberForm(Form):
def __init__(self, validate_code_func, *args, **kwargs):
self.validate_code_func = validate_code_func
super(ConfirmMobileNumberForm, self).__init__(*args, **kwargs)
sms_code = sms_code()
def validate_sms_code(self, field):
is_valid, msg = self.validate_code_func(field.data)
if not is_valid:
raise ValidationError(msg)
class CreateKeyForm(Form):
def __init__(self, existing_key_names=[], *args, **kwargs):

View File

@@ -14,32 +14,6 @@ class Blacklist(object):
raise ValidationError(self.message)
class ValidateUserCodes(object):
def __init__(self,
expiry_msg='Code has expired',
invalid_msg='Code does not match',
code_type=None):
self.expiry_msg = expiry_msg
self.invalid_msg = invalid_msg
self.code_type = code_type
def __call__(self, form, field):
# TODO would be great to do this sql query but
# not couple those parts of the code.
user_codes = getattr(form, 'user_codes', [])
valid_code = False
for code in user_codes:
if check_hash(field.data, code.code) and self.code_type == code.code_type:
if code.expiry_datetime <= datetime.now():
raise ValidationError(self.expiry_msg)
else:
# Valid code
valid_code = True
break
if not valid_code:
raise ValidationError(self.invalid_msg)
class CsvFileValidator(object):
def __init__(self, message='Not a csv file'):

View File

@@ -4,17 +4,16 @@ from flask import (
from app.main import main
from app.main.dao import users_dao
from app.main.forms import EmailNotReceivedForm, TextNotReceivedForm
from app.notify_client.sender import send_sms_code, send_email_code
@main.route('/email-not-received', methods=['GET', 'POST'])
def check_and_resend_email_code():
# TODO there needs to be a way to regenerate a session id
user = users_dao.get_user_by_email(session['user_email'])
user = users_dao.get_user_by_email(session['user_details']['email'])
form = EmailNotReceivedForm(email_address=user.email_address)
if form.validate_on_submit():
users_dao.update_email_address(id=user.id, email_address=form.email_address.data)
send_email_code(user_id=user.id, email=user.email_address)
users_dao.send_verify_code(user.id, 'email')
return redirect(url_for('.verify'))
return render_template('views/email-not-received.html', form=form)
@@ -22,11 +21,11 @@ def check_and_resend_email_code():
@main.route('/text-not-received', methods=['GET', 'POST'])
def check_and_resend_text_code():
# TODO there needs to be a way to regenerate a session id
user = users_dao.get_user_by_email(session['user_email'])
user = users_dao.get_user_by_email(session['user_details']['email'])
form = TextNotReceivedForm(mobile_number=user.mobile_number)
if form.validate_on_submit():
users_dao.update_mobile_number(id=user.id, mobile_number=form.mobile_number.data)
send_sms_code(user_id=user.id, mobile_number=user.mobile_number)
users_dao.send_verify_code(user.id, 'sms')
return redirect(url_for('.verify'))
return render_template('views/text-not-received.html', form=form)
@@ -39,6 +38,6 @@ def verification_code_not_received():
@main.route('/send-new-code', methods=['GET'])
def check_and_resend_verification_code():
# TODO there needs to be a way to generate a new session id
user = users_dao.get_user_by_email(session['user_email'])
send_sms_code(user.id, user.mobile_number)
user = users_dao.get_user_by_email(session['user_details']['email'])
users_dao.send_verify_code(user.id, 'sms')
return redirect(url_for('main.two_factor'))

View File

@@ -3,7 +3,7 @@ from flask import (render_template, url_for, redirect, flash)
from app.main import main
from app.main.dao import users_dao
from app.main.forms import NewPasswordForm
from app.notify_client.sender import check_token, send_sms_code
from app.notify_client.sender import check_token
@main.route('/new-password/<path:token>', methods=['GET', 'POST'])
@@ -22,7 +22,7 @@ def new_password(token):
if form.validate_on_submit():
users_dao.update_password(user, form.new_password.data)
send_sms_code(user.id, user.mobile_number)
users_dao.send_verify_code(user.id, 'sms')
return redirect(url_for('main.two_factor'))
else:
return render_template('views/new-password.html', token=token, form=form, user=user)

View File

@@ -4,7 +4,8 @@ from flask import (
render_template,
redirect,
session,
abort
abort,
url_for
)
from client.errors import HTTPError
@@ -15,10 +16,6 @@ from app.main.forms import RegisterUserForm
from app import user_api_client
# TODO how do we handle duplicate unverifed email addresses?
# malicious or otherwise.
from app.notify_client.sender import send_sms_code, send_email_code
@main.route('/register', methods=['GET', 'POST'])
def register():
@@ -41,10 +38,10 @@ def register():
# How do we report to the user there is a problem with
# sending codes apart from service unavailable?
# at the moment i believe http 500 is fine.
send_sms_code(user_id=user.id, mobile_number=user.mobile_number)
send_email_code(user_id=user.id, email=user.email_address)
users_dao.send_verify_code(user.id, 'sms')
users_dao.send_verify_code(user.id, 'email')
session['expiry_date'] = str(datetime.now() + timedelta(hours=1))
session['user_details'] = {"email": user.email_address, "id": user.id}
return redirect('/verify')
return redirect(url_for('main.verify'))
return render_template('views/register.html', form=form)

View File

@@ -6,32 +6,25 @@ from flask import (
abort
)
from app.main import main
from app.main.dao import users_dao
from app.main.forms import LoginForm
from app.notify_client.sender import send_sms_code
@main.route('/sign-in', methods=(['GET', 'POST']))
def sign_in():
try:
form = LoginForm()
if form.validate_on_submit():
user = users_dao.get_user_by_email(form.email_address.data)
if user:
if not user.is_locked() and user.is_active() and users_dao.verify_password(user, form.password.data):
send_sms_code(user.id, user.mobile_number)
session['user_email'] = user.email_address
return redirect(url_for('.two_factor'))
else:
# TODO re wire this increment to api
users_dao.increment_failed_login_count(user.id)
# Vague error message for login
form.password.errors.append('Username or password is incorrect')
form = LoginForm()
if form.validate_on_submit():
user = users_dao.get_user_by_email(form.email_address.data)
if user:
if not user.is_locked() and user.is_active() and users_dao.verify_password(user, form.password.data):
users_dao.send_verify_code(user.id, 'sms')
session['user_details'] = {"email": user.email_address, "id": user.id}
return redirect(url_for('.two_factor'))
else:
# TODO re wire this increment to api
users_dao.increment_failed_login_count(user.id)
# Vague error message for login
form.password.errors.append('Username or password is incorrect')
return render_template('views/signin.html', form=form)
except:
import traceback
traceback.print_exc()
abort(500)
return render_template('views/signin.html', form=form)

View File

@@ -5,19 +5,23 @@ from flask import (
from flask_login import login_user
from app.main import main
from app.main.dao import users_dao, verify_codes_dao
from app.main.dao import users_dao
from app.main.forms import TwoFactorForm
@main.route('/two-factor', methods=['GET', 'POST'])
def two_factor():
# TODO handle user_email not in session
user = users_dao.get_user_by_email(session['user_email'])
codes = verify_codes_dao.get_codes(user.id)
form = TwoFactorForm(codes)
user_id = session['user_details']['id']
def _check_code(code):
return users_dao.check_verify_code(user_id, code, "sms")
form = TwoFactorForm(_check_code)
if form.validate_on_submit():
verify_codes_dao.use_code_for_user_and_type(user_id=user.id, code_type='sms')
del session['user_details']
user = users_dao.get_user_by_id(user_id)
login_user(user)
return redirect(url_for('.choose_service'))

View File

@@ -1,8 +1,10 @@
from flask import (
request, render_template, redirect, url_for, session)
from flask.ext.login import current_user
from flask_login import login_required
from app.main import main
from app.main.dao.users_dao import (verify_password, update_user)
from app.main.dao.users_dao import (
verify_password, update_user, check_verify_code, is_email_unique)
from app.main.forms import (
ChangePasswordForm, ChangeNameForm, ChangeEmailForm, ConfirmEmailForm,
ChangeMobileNumberForm, ConfirmMobileNumberForm, ConfirmPasswordForm
@@ -15,17 +17,19 @@ NEW_MOBILE_PASSWORD_CONFIRMED = 'new-mob-password-confirmed'
@main.route("/user-profile")
@login_required
def user_profile():
return render_template('views/user-profile.html')
@main.route("/user-profile/name", methods=['GET', 'POST'])
@login_required
def user_profile_name():
form = ChangeNameForm(new_name=current_user.name)
if form.validate_on_submit():
current_user.name = form.new_name
current_user.name = form.new_name.data
update_user(current_user)
return redirect(url_for('.user_profile'))
@@ -37,9 +41,13 @@ def user_profile_name():
@main.route("/user-profile/email", methods=['GET', 'POST'])
@login_required
def user_profile_email():
form = ChangeEmailForm(email_address=current_user.email_address)
def _is_email_unique(email):
return is_email_unique(email)
form = ChangeEmailForm(_is_email_unique,
email_address=current_user.email_address)
if form.validate_on_submit():
session[NEW_EMAIL] = form.email_address.data
@@ -52,6 +60,7 @@ def user_profile_email():
@main.route("/user-profile/email/authenticate", methods=['GET', 'POST'])
@login_required
def user_profile_email_authenticate():
# Validate password for form
@@ -75,18 +84,21 @@ def user_profile_email_authenticate():
@main.route("/user-profile/email/confirm", methods=['GET', 'POST'])
@login_required
def user_profile_email_confirm():
# TODO add verify code support
form = ConfirmEmailForm()
# Validate verify code for form
def _check_code(cde):
return check_verify_code(current_user.id, cde, 'email')
form = ConfirmEmailForm(_check_code)
if NEW_EMAIL_PASSWORD_CONFIRMED not in session:
return redirect('main.user_profile_email_authenticate')
if form.validate_on_submit():
current_user.email_address = session[NEW_EMAIL]
del session[NEW_EMAIL]
del session[NEW_EMAIL_PASSWORD_CONFIRMED]
current_user.email_address = session['new_email']
update_user(current_user)
return redirect(url_for('.user_profile'))
@@ -98,6 +110,7 @@ def user_profile_email_confirm():
@main.route("/user-profile/mobile-number", methods=['GET', 'POST'])
@login_required
def user_profile_mobile_number():
form = ChangeMobileNumberForm(mobile_number=current_user.mobile_number)
@@ -114,6 +127,7 @@ def user_profile_mobile_number():
@main.route("/user-profile/mobile-number/authenticate", methods=['GET', 'POST'])
@login_required
def user_profile_mobile_number_authenticate():
# Validate password for form
@@ -137,14 +151,22 @@ def user_profile_mobile_number_authenticate():
@main.route("/user-profile/mobile-number/confirm", methods=['GET', 'POST'])
@login_required
def user_profile_mobile_number_confirm():
form = ConfirmMobileNumberForm()
# Validate verify code for form
def _check_code(cde):
return check_verify_code(current_user, cde, 'sms')
if NEW_MOBILE_PASSWORD_CONFIRMED not in session:
return redirect(url_for('.user_profile_mobile_number'))
form = ConfirmMobileNumberForm(_check_code)
if form.validate_on_submit():
current_user.mobile = session[NEW_MOBILE]
del session[NEW_MOBILE]
del session[NEW_MOBILE_PASSWORD_CONFIRMED]
current_user.mobile_user
update_user(current_user)
return redirect(url_for('.user_profile'))
@@ -156,11 +178,17 @@ def user_profile_mobile_number_confirm():
@main.route("/user-profile/password", methods=['GET', 'POST'])
@login_required
def user_profile_password():
form = ChangePasswordForm()
# Validate password for form
def _check_password(pwd):
return verify_password(current_user, pwd)
form = ChangePasswordForm(_check_password)
if form.validate_on_submit():
current_user.set_password(form.new_password.data)
update_user(current_user)
return redirect(url_for('.user_profile'))
return render_template(

View File

@@ -11,7 +11,7 @@ from client.errors import HTTPError
from flask_login import login_user
from app.main import main
from app.main.dao import users_dao, verify_codes_dao
from app.main.dao import users_dao
from app.main.forms import VerifyForm
@@ -20,12 +20,11 @@ def verify():
# TODO there needs to be a way to regenerate a session id
# or handle gracefully.
user_id = session['user_details']['id']
codes = verify_codes_dao.get_codes(user_id)
form = VerifyForm(codes)
if form.validate_on_submit():
verify_codes_dao.use_code_for_user_and_type(user_id=user_id, code_type='email')
verify_codes_dao.use_code_for_user_and_type(user_id=user_id, code_type='sms')
def _check_code(code, code_type):
return users_dao.check_verify_code(user_id, code, code_type)
form = VerifyForm(_check_code)
if form.validate_on_submit():
try:
user = users_dao.get_user_by_id(user_id)
activated_user = users_dao.activate_user(user)