Skip to content

Commit

Permalink
add user authentication and test case for user
Browse files Browse the repository at this point in the history
  • Loading branch information
lesliebinbin committed Aug 23, 2018
1 parent c418afe commit d9c300b
Show file tree
Hide file tree
Showing 15 changed files with 157 additions and 4 deletions.
6 changes: 6 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
from config import config
from flask_login import LoginManager

bootstrap = Bootstrap()
mail = Mail()
moment = Moment()
db = SQLAlchemy()
login_manager = LoginManager()
login_manager.login_view = "auth.login"


def create_app(config_name):
Expand All @@ -19,6 +22,9 @@ def create_app(config_name):
mail.init_app(app)
moment.init_app(app)
db.init_app(app)
login_manager.init_app(app)
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix="/auth")
return app
Binary file modified app/__pycache__/__init__.cpython-36.pyc
Binary file not shown.
Binary file modified app/__pycache__/models.cpython-36.pyc
Binary file not shown.
3 changes: 3 additions & 0 deletions app/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from flask import Blueprint
auth = Blueprint('auth', __name__)
from . import views
13 changes: 13 additions & 0 deletions app/auth/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email


class LoginForm(FlaskForm):
email = StringField(
'Email', validators=[DataRequired(),
Length(1, 64),
Email()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Keep me logged in')
submit = SubmitField('Log In')
20 changes: 20 additions & 0 deletions app/auth/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from flask import render_template, redirect, request, url_for, flash
from . import auth
from flask_login import login_user
from ..models import User
from .forms import LoginForm


@auth.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and user.verify_password(form.password.data):
login_user(user, form.remember_me.data)
next = request.args.get('next')
if not next or not next.startswith('/'):
next = url_for('main.index')
return redirect(next)
flash('Invalid username or password')
return render_template('auth/login.html', form=form)
Binary file modified app/main/__pycache__/views.cpython-36.pyc
Binary file not shown.
27 changes: 25 additions & 2 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from . import db
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from . import login_manager


class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
Expand All @@ -9,11 +14,29 @@ def __repr__(self):
return f"<Role {self.name}>"


class User(db.Model):
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
password_hash = db.Column(db.String(128))
email = db.Column(db.String(64), unique=True, index=True)

@property
def password(self):
raise AttributeError("password is not a readable attribute")

@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password=password)

def verify_password(self, password):
return check_password_hash(self.password_hash, password)

def __repr__(self):
return f"<User {self.username}>"
return f"<User {self.username}>"


@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
Empty file added app/templates/auth/login.html
Empty file.
9 changes: 8 additions & 1 deletion app/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
<li><a href="{{url_for('main.index')}}">Home</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{%if current_user.is_authenticated%}
<li><a href="{{url_for('auth.logout')}}">Log out</a></li>
{%else%}
<li><a href="{{url_for('auth.login')}}">Log in</a></li>
{%endif%}
</ul>
</div>
</div>
Expand Down
Binary file modified data-dev.sqlite
Binary file not shown.
2 changes: 1 addition & 1 deletion flasky.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test():


def main():
app.run(host='0.0.0.0', port=8080, debug=True)
app.run(host='0.0.0.0', debug=True)


if __name__ == "__main__":
Expand Down
28 changes: 28 additions & 0 deletions migrations/versions/1245c4761d44_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""empty message
Revision ID: 1245c4761d44
Revises: 968188240638
Create Date: 2018-08-23 11:48:42.441637
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '1245c4761d44'
down_revision = '968188240638'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('password_hash', sa.String(length=128), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'password_hash')
# ### end Alembic commands ###
30 changes: 30 additions & 0 deletions migrations/versions/8c77319f915c_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""empty message
Revision ID: 8c77319f915c
Revises: 1245c4761d44
Create Date: 2018-08-23 12:34:15.969979
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '8c77319f915c'
down_revision = '1245c4761d44'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('email', sa.String(length=64), nullable=True))
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_column('users', 'email')
# ### end Alembic commands ###
23 changes: 23 additions & 0 deletions tests/test_user_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import unittest
from app.models import User


class UserModelTestCase(unittest.TestCase):
def test_password_setter(self):
u = User(password='cat')
self.assertTrue(u.password_hash is not None)

def test_no_password_getter(self):
u = User(password='cat')
with self.assertRaises(AttributeError):
u.password

def test_password_verification(self):
u = User(password='cat')
self.assertTrue(u.verify_password('cat'))
self.assertFalse(u.verify_password('dog'))

def test_password_salts_are_random(self):
u = User(password='cat')
u2 = User(password='cat')
self.assertTrue(u.password_hash != u2.password_hash)

0 comments on commit d9c300b

Please sign in to comment.