Implement user login based on sessions
Use Flask-Login & Flask-WTF libraries
This commit is contained in:
parent
95cc6f5e94
commit
3af67fee01
|
@ -0,0 +1,62 @@
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
from wtforms import EmailField, PasswordField, validators
|
||||||
|
|
||||||
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class LoginForm(FlaskForm):
|
||||||
|
email = EmailField('Email Address', [validators.Length(min=6, max=35)])
|
||||||
|
password = PasswordField('Password', [
|
||||||
|
validators.DataRequired(),
|
||||||
|
])
|
||||||
|
|
||||||
|
error_messages = {
|
||||||
|
'invalid_login': (
|
||||||
|
"Please enter a correct email and password. Note that both "
|
||||||
|
"fields may be case-sensitive."
|
||||||
|
),
|
||||||
|
'inactive': "This account is inactive.",
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate(self, extra_validators=None):
|
||||||
|
is_valid = super().validate(extra_validators)
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
|
return False
|
||||||
|
|
||||||
|
email = self.email.data
|
||||||
|
password = self.password.data
|
||||||
|
self.user_cache = self.authenticate(email, password)
|
||||||
|
|
||||||
|
if self.user_cache is None:
|
||||||
|
self.form_errors.append(self.error_messages['invalid_login'])
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.confirm_login_allowed(self.user_cache)
|
||||||
|
|
||||||
|
def authenticate(self, email, password):
|
||||||
|
if email is None or password is None:
|
||||||
|
return
|
||||||
|
user = User.query.filter_by(email=email).first()
|
||||||
|
if user is None:
|
||||||
|
# Run the default password hasher once to reduce the timing
|
||||||
|
# difference between an existing and a nonexistent user (#20760).
|
||||||
|
generate_password_hash(password)
|
||||||
|
else:
|
||||||
|
if user.check_password(password):
|
||||||
|
return user
|
||||||
|
|
||||||
|
def confirm_login_allowed(self, user):
|
||||||
|
"""
|
||||||
|
Controls whether the given User may log in. This is a policy setting,
|
||||||
|
independent of end-user authentication. This default behavior is to
|
||||||
|
allow login by active users, and reject login by inactive users.
|
||||||
|
If the given user cannot log in, this method should raise a
|
||||||
|
``ValidationError``.
|
||||||
|
If the given user may log in, this method should return None.
|
||||||
|
"""
|
||||||
|
if not user.is_active:
|
||||||
|
self.form_errors.append(self.error_messages['inactive'])
|
||||||
|
|
||||||
|
return user.is_active
|
|
@ -1,7 +1,7 @@
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from citext import CIText
|
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
|
from flask_login import UserMixin
|
||||||
from sqlalchemy import Column, Boolean, BigInteger, Sequence
|
from sqlalchemy import Column, Boolean, BigInteger, Sequence
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy_utils import EmailType, PasswordType
|
from sqlalchemy_utils import EmailType, PasswordType
|
||||||
|
@ -13,7 +13,7 @@ from ereuse_devicehub.resources.models import STR_SIZE, Thing
|
||||||
from ereuse_devicehub.resources.enums import SessionType
|
from ereuse_devicehub.resources.enums import SessionType
|
||||||
|
|
||||||
|
|
||||||
class User(Thing):
|
class User(UserMixin, Thing):
|
||||||
__table_args__ = {'schema': 'common'}
|
__table_args__ = {'schema': 'common'}
|
||||||
id = Column(UUID(as_uuid=True), default=uuid4, primary_key=True)
|
id = Column(UUID(as_uuid=True), default=uuid4, primary_key=True)
|
||||||
email = Column(EmailType, nullable=False, unique=True)
|
email = Column(EmailType, nullable=False, unique=True)
|
||||||
|
@ -66,6 +66,15 @@ class User(Thing):
|
||||||
return
|
return
|
||||||
return self.email.split('@')[0].split('_')[1]
|
return self.email.split('@')[0].split('_')[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self):
|
||||||
|
"""Alias because flask-login expects `is_active` attribute"""
|
||||||
|
return self.active
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
# take advantage of SQL Alchemy PasswordType to verify password
|
||||||
|
return self.password == password
|
||||||
|
|
||||||
|
|
||||||
class UserInventory(db.Model):
|
class UserInventory(db.Model):
|
||||||
"""Relationship between users and their inventories."""
|
"""Relationship between users and their inventories."""
|
||||||
|
|
|
@ -22,22 +22,30 @@
|
||||||
<div class="pt-4 pb-2">
|
<div class="pt-4 pb-2">
|
||||||
<h5 class="card-title text-center pb-0 fs-4">Login to Your Account</h5>
|
<h5 class="card-title text-center pb-0 fs-4">Login to Your Account</h5>
|
||||||
<p class="text-center small">Enter your username & password to login</p>
|
<p class="text-center small">Enter your username & password to login</p>
|
||||||
|
{% if form.form_errors %}
|
||||||
|
<p class="text-danger">
|
||||||
|
{% for error in form.form_errors %}
|
||||||
|
{{ error }}<br/>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="row g-3 needs-validation" novalidate>
|
<form method="post" class="row g-3 needs-validation" novalidate>
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label for="yourUsername" class="form-label">Username</label>
|
<label for="yourUsername" class="form-label">Email</label>
|
||||||
<div class="input-group has-validation">
|
<div class="input-group has-validation">
|
||||||
<span class="input-group-text" id="inputGroupPrepend">@</span>
|
<span class="input-group-text" id="inputGroupPrepend">@</span>
|
||||||
<input type="text" name="username" class="form-control" id="yourUsername" required>
|
<input type="text" name="email" class="form-control" id="yourUsername" required value="{{ form.email.data|default('', true) }}">
|
||||||
<div class="invalid-feedback">Please enter your username.</div>
|
<div class="invalid-feedback">Please enter your username.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label for="yourPassword" class="form-label">Password</label>
|
<label for="yourPassword" class="form-label">Password</label>
|
||||||
<input type="password" name="passwword" class="form-control" id="yourPassword" required>
|
<input type="password" name="password" class="form-control" id="yourPassword" required>
|
||||||
<div class="invalid-feedback">Please enter your password!</div>
|
<div class="invalid-feedback">Please enter your password!</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
from urllib.parse import urljoin, urlparse
|
||||||
|
|
||||||
|
|
||||||
|
def is_safe_url(request, target):
|
||||||
|
ref_url = urlparse(request.host_url)
|
||||||
|
test_url = urlparse(urljoin(request.host_url, target))
|
||||||
|
return test_url.scheme in ('http', 'https') and \
|
||||||
|
ref_url.netloc == test_url.netloc
|
|
@ -1,22 +1,43 @@
|
||||||
from flask import Blueprint, render_template
|
import flask
|
||||||
|
from flask import Blueprint
|
||||||
from flask.views import View
|
from flask.views import View
|
||||||
|
from flask_login import login_user
|
||||||
|
|
||||||
|
from ereuse_devicehub.forms import LoginForm
|
||||||
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
from ereuse_devicehub.utils import is_safe_url
|
||||||
|
|
||||||
core = Blueprint('core', __name__)
|
core = Blueprint('core', __name__)
|
||||||
|
|
||||||
|
|
||||||
class LoginView(View):
|
class LoginView(View):
|
||||||
|
methods = ['GET', 'POST']
|
||||||
template_name = 'ereuse_devicehub/user_login.html'
|
template_name = 'ereuse_devicehub/user_login.html'
|
||||||
|
|
||||||
def dispatch_request(self):
|
def dispatch_request(self):
|
||||||
return render_template(self.template_name)
|
form = LoginForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
# Login and validate the user.
|
||||||
|
# user should be an instance of your `User` class
|
||||||
|
user = User.query.filter_by(email=form.email.data).first()
|
||||||
|
login_user(user)
|
||||||
|
|
||||||
|
next_url = flask.request.args.get('next')
|
||||||
|
# is_safe_url should check if the url is safe for redirects.
|
||||||
|
# See http://flask.pocoo.org/snippets/62/ for an example.
|
||||||
|
if not is_safe_url(flask.request, next_url):
|
||||||
|
return flask.abort(400)
|
||||||
|
|
||||||
|
return flask.redirect(next_url or flask.url_for('core.user-profile'))
|
||||||
|
return flask.render_template('ereuse_devicehub/user_login.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
class UserProfileView(View):
|
class UserProfileView(View):
|
||||||
template_name = 'ereuse_devicehub/user_profile.html'
|
template_name = 'ereuse_devicehub/user_profile.html'
|
||||||
|
|
||||||
def dispatch_request(self):
|
def dispatch_request(self):
|
||||||
return render_template(self.template_name)
|
context = {}
|
||||||
|
return flask.render_template(self.template_name, **context)
|
||||||
|
|
||||||
|
|
||||||
core.add_url_rule('/login/', view_func=LoginView.as_view('login'))
|
core.add_url_rule('/login/', view_func=LoginView.as_view('login'))
|
||||||
|
|
|
@ -9,7 +9,9 @@ colour==0.1.5
|
||||||
ereuse-utils[naming,test,session,cli]==0.4.0b49
|
ereuse-utils[naming,test,session,cli]==0.4.0b49
|
||||||
Flask==1.0.2
|
Flask==1.0.2
|
||||||
Flask-Cors==3.0.6
|
Flask-Cors==3.0.6
|
||||||
|
Flask-Login==0.5.0
|
||||||
Flask-SQLAlchemy==2.3.2
|
Flask-SQLAlchemy==2.3.2
|
||||||
|
Flask-WTF==1.0.0
|
||||||
hashids==1.2.0
|
hashids==1.2.0
|
||||||
inflection==0.3.1
|
inflection==0.3.1
|
||||||
marshmallow==3.0.0b11
|
marshmallow==3.0.0b11
|
||||||
|
|
Reference in New Issue