Skip to content

Commit

Permalink
Add auth
Browse files Browse the repository at this point in the history
Closes #34
  • Loading branch information
emillon committed Oct 30, 2014
1 parent e248195 commit f3690de
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 2 deletions.
88 changes: 88 additions & 0 deletions app/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from flask import Blueprint, render_template, flash, redirect, url_for, request
from flask.ext.login import LoginManager, login_user, logout_user
from flask.ext.wtf import Form
from wtforms import TextField, PasswordField
from wtforms.validators import Required, EqualTo
from models import db, User
import bcrypt
from sqlalchemy.orm.exc import NoResultFound

lm = LoginManager()

auth = Blueprint('auth', __name__)


def auth_user(login, password):
try:
user = db.session.query(User).filter(User.name == login).one()
except NoResultFound:
return None
db_hash = user.password
hashed = bcrypt.hashpw(password.encode('utf-8'), db_hash.encode('ascii'))
if db_hash != hashed:
return None
return user


class SignupForm(Form):
"""
Form used in signup
"""
username = TextField(validators=[Required()])
password = PasswordField(
validators=[Required(),
EqualTo('confirm', message='Passwords must match')])
confirm = PasswordField(validators=[Required()])


@auth.route('/signup', methods=['GET', 'POST'])
def signup():
"""
Sign up a new user
"""
form = SignupForm()
if form.validate_on_submit():
username = form.username.data
password = form.password.data
user = User(username, password)
db.session.add(user)
db.session.commit()
flash('User successfully created')
return redirect(url_for('bp.home'))
return render_template('signup.html', title='Sign up', form=form)


class LoginForm(Form):
"""
Form used in login
"""
username = TextField(validators=[Required()])
password = PasswordField(validators=[Required()])


@auth.route('/login', methods=['GET', 'POST'])
def login():
"""
Log a user in
"""
form = LoginForm()
if form.validate_on_submit():
username = form.username.data
password = form.password.data
user = auth_user(username, password)
if user is None:
flash('Bad login or password')
return redirect(url_for('.login'))
login_user(user)
flash('Logged in')
return redirect(request.args.get('next') or url_for('bp.home'))
return render_template('login.html', title='Log in', form=form)


@auth.route('/logout')
def logout():
"""
Log a user out
"""
logout_user()
return redirect(url_for('bp.home'))
21 changes: 19 additions & 2 deletions app/factory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from flask import Flask
from flask import Flask, g
import os
from key import get_secret_key
from uploads import documents
Expand All @@ -8,6 +8,8 @@
from flask.ext.migrate import Migrate, MigrateCommand
from flask.ext.admin import Admin
from flask.ext.admin.contrib.sqla import ModelView
from flask.ext.login import current_user
from auth import lm

def create_app(config_file=None):

Expand Down Expand Up @@ -50,6 +52,19 @@ def create_app(config_file=None):
# flask-migrate
migrate = Migrate(app, models.db)

# auth
lm.init_app(app)
@lm.user_loader
def load_user(userid):
"""
Needed for flask-login.
"""
return models.User.query.get(int(userid))

@app.before_request
def set_g_user():
g.user = current_user

# flask-admin
admin = Admin(app, name=app.name + ' Admin')
admin_models = [models.Document,
Expand All @@ -59,12 +74,14 @@ def create_app(config_file=None):

class RestrictedModelView(ModelView):
def is_accessible(self):
return True # FIXME when auth is done
return current_user.is_authenticated() and current_user.is_admin()

for model in admin_models:
admin.add_view(RestrictedModelView(model, models.db.session))

from views import bp
app.register_blueprint(bp)
from auth import auth
app.register_blueprint(auth)

return app
47 changes: 47 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,59 @@
"""
import random
from flask.ext.sqlalchemy import SQLAlchemy
import bcrypt


"""
The main DB object. It gets initialized in create_app.
"""
db = SQLAlchemy()


ROLE_USER = 0
ROLE_ADMIN = 1


class User(db.Model):
"""
Application user. Someone that can log in.
"""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True, nullable=False)
password = db.Column(db.String, nullable=False)
role = db.Column(db.SmallInteger, default=ROLE_USER, nullable=False)

def __init__(self, login, password, workfactor=12):
self.name = login
salt = bcrypt.gensalt(workfactor)
self.password = bcrypt.hashpw(password.encode('utf-8'), salt)

def is_active(self):
"""
Needed for flask-login.
"""
return True

def is_authenticated(self):
"""
Needed for flask-login.
"""
return True

def get_id(self):
"""
Needed for flask-login.
"""
return unicode(self.id)

def is_admin(self):
"""
Has the user got administrative rights?
This grants access to admin panel, so careful.
"""
return (self.role == ROLE_ADMIN)


"""
A document. The actual file is stored in the application's instance path.
"""
Expand Down
43 changes: 43 additions & 0 deletions app/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,46 @@ def test_upload_rev(self):
self.assert200(r)
for docb in [docid, docid2]:
self.assertIn(docb, r.data)

def _signup(self, username, password):
return self.client.post('/signup', data=dict(
username=username,
password=password,
confirm=password
), follow_redirects=True)

def _login(self, username, password, signup=False):
if signup:
self.signup(username, password)
return self.client.post('/login', data=dict(
username=username,
password=password
), follow_redirects=True)

def _logout(self):
return self.client.get('/logout', follow_redirects=True)

def test_signup_login_logout(self):
r = self.client.get('/')
self.assertIn('Log in', r.data)
self.assertNotIn('Log out', r.data)
self.assertNotIn('Admin panel', r.data)
r = self._signup('a', 'a')
self.assertIn('User successfully created', r.data)
r = self._login('a', 'a')
self.assertIn('Signed in as a', r.data)
self.assertNotIn('Log in', r.data)
self.assertIn('Log out', r.data)
self.assertNotIn('Admin panel', r.data)
r = self._logout()
self.assertIn('Log in', r.data)
self.assertNotIn('Log out', r.data)

def test_login_nonexistent(self):
r = self._login('doesnt', 'exist')
self.assertIn('Bad login or password', r.data)

def test_login_bad_pass(self):
self._signup('a', 'a')
r = self._login('a', 'b')
self.assertIn('Bad login or password', r.data)
8 changes: 8 additions & 0 deletions docs/app.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ app package
Submodules
----------

app.auth module
---------------

.. automodule:: app.auth
:members:
:undoc-members:
:show-inheritance:

app.factory module
------------------

Expand Down
33 changes: 33 additions & 0 deletions migrations/versions/4e32faff090b_add_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Add User
Revision ID: 4e32faff090b
Revises: 1c27bdf60a4e
Create Date: 2014-10-30 13:51:13.288615
"""

# revision identifiers, used by Alembic.
revision = '4e32faff090b'
down_revision = '1c27bdf60a4e'

from alembic import op
import sqlalchemy as sa


def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('password', sa.String(), nullable=False),
sa.Column('role', sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
### end Alembic commands ###


def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
### end Alembic commands ###
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
bcrypt
coverage
flask
flask-admin
flask-assets
flask-login
flask-migrate
flask-script
flask-sqlalchemy
Expand Down
18 changes: 18 additions & 0 deletions templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@
<div class="navbar-header">
<a class="navbar-brand" href="/">Review</a>
</div>
<div class="collapse navbar-collapse navbar-ex1-collapse">
{% if g.user.is_authenticated() %}
<ul class="nav navbar-nav">
{% if g.user.is_admin() %}
<li><a href="/admin">Admin panel</a></li>
{% endif %}
</ul>
{% endif %}
<ul class="nav navbar-nav navbar-right">
{% if g.user.is_authenticated() %}
<li><p class="navbar-text">Signed in as {{g.user.name}}</p></li>
<li><a href="{{url_for('auth.logout')}}">Log out</a></li>
{% else %}
<li><a href="{{url_for('auth.signup')}}">Sign up</a></li>
<li><a href="{{url_for('auth.login')}}">Log in</a></li>
{% endif %}
</ul>
</div>
</nav>
{% with messages = get_flashed_messages() %}
{% for message in messages %}
Expand Down
9 changes: 9 additions & 0 deletions templates/form_errors.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% if form.errors %}
<ul class="errors">
{% for field_name, field_errors in form.errors|dictsort if field_errors %}
{% for error in field_errors %}
<li>{{ form[field_name].label }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
{% endif %}
9 changes: 9 additions & 0 deletions templates/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block content %}
<form method="post">
{{form.hidden_tag()}}
<p> <label> Username {{form.username()}} </label> </p>
<p> <label> Password {{form.password()}} </label> </p>
<p><input type="submit" value="Log in"></p>
</form>
{% endblock %}
11 changes: 11 additions & 0 deletions templates/signup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block content %}
{% include "form_errors.html" %}
<form method="post">
{{form.hidden_tag()}}
<p> <label> Username {{form.username()}} </label> </p>
<p> <label> Password {{form.password()}} </label> </p>
<p> <label> Confirm password {{form.confirm()}} </label> </p>
<p><input type="submit" value="Log in"></p>
</form>
{% endblock %}

0 comments on commit f3690de

Please sign in to comment.