Basic auth system implimented
Basic user authantication has implimented with Flask-Login and pass word hasing with werkzeg.
This commit is contained in:
parent
e24f059bb0
commit
8b95f4c054
|
@ -1,8 +1,11 @@
|
||||||
from flask import Flask, render_template
|
from flask import Flask, render_template
|
||||||
|
|
||||||
from flask_adminlte import AdminLTE
|
from flask_adminlte import AdminLTE
|
||||||
from flask_mail import Mail
|
from flask_mail import Mail
|
||||||
from flask_moment import Moment
|
from flask_moment import Moment
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_login import LoginManager
|
||||||
|
|
||||||
from config import config
|
from config import config
|
||||||
|
|
||||||
AdminLTE = AdminLTE()
|
AdminLTE = AdminLTE()
|
||||||
|
@ -10,20 +13,37 @@ mail = Mail()
|
||||||
moment = Moment()
|
moment = Moment()
|
||||||
db = SQLAlchemy()
|
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):
|
def create_app(config_name):
|
||||||
dash = Flask(__name__)
|
dash = Flask(__name__)
|
||||||
dash.config.from_object(config[config_name])
|
dash.config.from_object(config[config_name])
|
||||||
config[config_name].init_app(dash)
|
config[config_name].init_app(dash)
|
||||||
|
|
||||||
|
|
||||||
AdminLTE.init_app(dash)
|
AdminLTE.init_app(dash)
|
||||||
mail.init_app(dash)
|
mail.init_app(dash)
|
||||||
moment.init_app(dash)
|
moment.init_app(dash)
|
||||||
db.init_app(dash)
|
db.init_app(dash)
|
||||||
|
login_manager.init_app(dash)
|
||||||
|
|
||||||
|
|
||||||
# attach routes and custom error pages here
|
# attach routes and custom error pages here
|
||||||
|
|
||||||
|
# main application
|
||||||
from main import main as main_blueprint
|
from main import main as main_blueprint
|
||||||
dash.register_blueprint(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
|
return dash
|
|
@ -0,0 +1,5 @@
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
auth = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
from . import views
|
|
@ -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')
|
||||||
|
|
|
@ -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'))
|
|
@ -1,4 +1,9 @@
|
||||||
|
import dateutil.parser
|
||||||
|
|
||||||
from flask import render_template, session, redirect, url_for, current_app
|
from flask import render_template, session, redirect, url_for, current_app
|
||||||
|
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
from .. import db
|
from .. import db
|
||||||
from ..models import User
|
from ..models import User
|
||||||
from ..email import send_email
|
from ..email import send_email
|
||||||
|
@ -7,31 +12,9 @@ from .forms import NameForm
|
||||||
|
|
||||||
|
|
||||||
@main.route('/', methods=['GET', 'POST'])
|
@main.route('/', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
form = NameForm()
|
return render_template('index.html')
|
||||||
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)
|
|
||||||
|
|
||||||
@main.route('/lockscreen')
|
@main.route('/lockscreen')
|
||||||
def lockscreen():
|
def lockscreen():
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
import dateutil.parser
|
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 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):
|
class Role(db.Model):
|
||||||
__tablename__ = 'roles'
|
__tablename__ = 'roles'
|
||||||
|
@ -14,15 +22,28 @@ class Role(db.Model):
|
||||||
return '<Role %r>' % self.name
|
return '<Role %r>' % self.name
|
||||||
|
|
||||||
|
|
||||||
class User(db.Model):
|
class User(UserMixin, db.Model):
|
||||||
__tablename__ = 'users'
|
__tablename__ = 'users'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
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)
|
username = db.Column(db.String(64), unique=True, index=True)
|
||||||
full_name = db.Column(db.String(256), index=True)
|
password_hash = db.Column(db.String(128))
|
||||||
avatar = db.Column(db.String(256), index=True)
|
full_name = db.Column(db.String(255), index=True)
|
||||||
created_at = dateutil.parser.parse("November 12, 2012")
|
avatar = db.Column(db.String(255), index=True)
|
||||||
|
created_at = db.Column(db.DateTime)
|
||||||
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
|
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):
|
def __repr__(self):
|
||||||
return '<User %r>' % self.username
|
return '<User %r>' % self.username
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<div class="login-logo">
|
<div class="login-logo">
|
||||||
<strong>Admin</strong>LTE
|
<strong>-</strong> stack
|
||||||
</div>
|
</div>
|
||||||
<div class="login-box-body">
|
<div class="login-box-body">
|
||||||
<p>Log into your account!</p>
|
<p>Log into your account!</p>
|
||||||
|
@ -23,17 +23,21 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{# Render the login form. #}
|
{# Render the login form. #}
|
||||||
<form method="post">
|
<form method="post" name="login">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
<div class="form-group has-feedback">
|
<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>
|
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group has-feedback">
|
<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>
|
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<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>
|
<button type="submit" class="btn btn-primary btn-block btn-flat">Log In</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -233,7 +233,9 @@
|
||||||
<a href="#" class="btn btn-default btn-flat">Profile</a>
|
<a href="#" class="btn btn-default btn-flat">Profile</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -272,7 +274,7 @@
|
||||||
</a>
|
</a>
|
||||||
<ul class="treeview-menu">
|
<ul class="treeview-menu">
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for('.login') }}">
|
<a href="{{ url_for('auth.login') }}">
|
||||||
<i class="fa fa-circle-o"></i>
|
<i class="fa fa-circle-o"></i>
|
||||||
Login
|
Login
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -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 ###
|
|
@ -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 ###
|
|
@ -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)
|
Loading…
Reference in New Issue