Basic auth system implimented

Basic user authantication has implimented with Flask-Login and
pass word hasing with werkzeg.
This commit is contained in:
kelepirci 2016-07-10 19:08:31 +03:00
parent e24f059bb0
commit 8b95f4c054
11 changed files with 186 additions and 37 deletions

View File

@ -1,8 +1,11 @@
from flask import Flask, render_template
from flask_adminlte import AdminLTE
from flask_mail import Mail
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from config import config
AdminLTE = AdminLTE()
@ -10,20 +13,37 @@ mail = Mail()
moment = Moment()
db = SQLAlchemy()
# initialize flask_login
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
# user loader call back function
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
def create_app(config_name):
dash = Flask(__name__)
dash.config.from_object(config[config_name])
config[config_name].init_app(dash)
AdminLTE.init_app(dash)
mail.init_app(dash)
moment.init_app(dash)
db.init_app(dash)
login_manager.init_app(dash)
# attach routes and custom error pages here
# main application
from main import main as main_blueprint
dash.register_blueprint(main_blueprint)
# auth application
from .auth import auth as auth_blueprint
dash.register_blueprint(auth_blueprint, url_prefix='/auth')
return dash

5
dash/auth/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from flask import Blueprint
auth = Blueprint('auth', __name__)
from . import views

12
dash/auth/forms.py Normal file
View File

@ -0,0 +1,12 @@
from flask_wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import Required, Length, Email
class LoginForm(Form):
email = StringField('Email', validators=[Required(), Length(1, 128),
Email()])
password = PasswordField('Password', validators=[Required()])
remember_me = BooleanField('Keep me logged in')
submit = SubmitField('Log In')

24
dash/auth/views.py Normal file
View File

@ -0,0 +1,24 @@
from flask import render_template, redirect, request, url_for, flash
from flask_login import login_user, logout_user, login_required
from . import auth
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 is not None and user.verify_password(form.password.data):
login_user(user, form.remember_me.data)
return redirect(request.args.get('next') or url_for('main.index'))
flash('Invalid username or password.')
return render_template('auth/login.html', form=form)
@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('You have been logged out.')
return redirect(url_for('main.index'))

View File

@ -1,4 +1,9 @@
import dateutil.parser
from flask import render_template, session, redirect, url_for, current_app
from flask_login import login_required
from .. import db
from ..models import User
from ..email import send_email
@ -7,31 +12,9 @@ from .forms import NameForm
@main.route('/', methods=['GET', 'POST'])
@login_required
def index():
form = NameForm()
current_user = User()
if form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first()
if user is None:
user = User(username=form.name.data)
db.session.add(user)
session['known'] = False
if current_app.config['DASH_STACK_ADMIN']:
send_email(current_app.config['DASH_STACK_ADMIN'], 'New User',
'mail/new_user', user=user)
else:
session['known'] = True
session['name'] = form.name.data
return redirect(url_for('.index'))
return render_template('index.html',
form=form, name=session.get('name'),
known=session.get('known', False),
current_user=current_user)
@main.route('/login')
def login():
current_user = User()
return render_template('login.html', current_user=current_user)
return render_template('index.html')
@main.route('/lockscreen')
def lockscreen():

View File

@ -1,7 +1,15 @@
import dateutil.parser
import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from . import db
from . import login_manager
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
class Role(db.Model):
__tablename__ = 'roles'
@ -14,15 +22,28 @@ class Role(db.Model):
return '<Role %r>' % self.name
class User(db.Model):
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(128), unique=True, index=True)
username = db.Column(db.String(64), unique=True, index=True)
full_name = db.Column(db.String(256), index=True)
avatar = db.Column(db.String(256), index=True)
created_at = dateutil.parser.parse("November 12, 2012")
password_hash = db.Column(db.String(128))
full_name = db.Column(db.String(255), index=True)
avatar = db.Column(db.String(255), index=True)
created_at = db.Column(db.DateTime)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
@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)
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return '<User %r>' % self.username
return '<User %r>' % self.username

View File

@ -8,7 +8,7 @@
<div class="login-box">
<div class="login-logo">
<strong>Admin</strong>LTE
<strong>-</strong> stack
</div>
<div class="login-box-body">
<p>Log into your account!</p>
@ -23,17 +23,21 @@
{% endif %}
{% endwith %}
{# Render the login form. #}
<form method="post">
<form method="post" name="login">
{{ form.hidden_tag() }}
<div class="form-group has-feedback">
<input type="email" placeholder="Username or Email" required="true" class="form-control">
<input type="email" name="email" placeholder="Email" required="true" class="form-control">
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<input type="password" placeholder="Password" required="true" class="form-control">
<input type="password" name="password" placeholder="Password" required="true" class="form-control">
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
<div class="row">
<div class="col-xs-4 col-xs-offset-8">
<div class="col-xs-6 col-xs-offset-0">
<p><input type="checkbox" name="remember_me" /> Remember me?</p>
</div>
<div class="col-xs-6 col-xs-offset-0">
<button type="submit" class="btn btn-primary btn-block btn-flat">Log In</button>
</div>
</div>

View File

@ -233,7 +233,9 @@
<a href="#" class="btn btn-default btn-flat">Profile</a>
</div>
<div class="pull-right">
<a href="{{ url_for('.login') }}" class="btn btn-default btn-flat">Sign out</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('auth.logout') }}" class="btn btn-default btn-flat">Sign out</a>
{% endif %}
</div>
</li>
</ul>
@ -272,7 +274,7 @@
</a>
<ul class="treeview-menu">
<li>
<a href="{{ url_for('.login') }}">
<a href="{{ url_for('auth.login') }}">
<i class="fa fa-circle-o"></i>
Login
</a>

View File

@ -0,0 +1,30 @@
"""empty message
Revision ID: 24ea14f8cb4e
Revises: 8491b2bf3af9
Create Date: 2016-07-09 23:21:24.446391
"""
# revision identifiers, used by Alembic.
revision = '24ea14f8cb4e'
down_revision = '8491b2bf3af9'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('email', sa.String(length=128), nullable=True))
op.add_column('users', sa.Column('password_hash', sa.String(length=128), 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', 'password_hash')
op.drop_column('users', 'email')
### end Alembic commands ###

View File

@ -0,0 +1,26 @@
"""empty message
Revision ID: 28f38d9e7724
Revises: 24ea14f8cb4e
Create Date: 2016-07-10 18:31:53.096426
"""
# revision identifiers, used by Alembic.
revision = '28f38d9e7724'
down_revision = '24ea14f8cb4e'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('created_at', sa.DateTime(), nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'created_at')
### end Alembic commands ###

22
tests/test_user_model.py Normal file
View File

@ -0,0 +1,22 @@
import unittest
from dash.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)