authorization routes and logic
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from distutils.command.config import config
|
||||
from flask import Flask
|
||||
from flask_login import LoginManager
|
||||
from flask_mail import Mail
|
||||
from flask_moment import Moment
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
@@ -10,6 +11,8 @@ from config import configuration, DevelopmentConfig, TestingConfig, ProductionCo
|
||||
mail =Mail()
|
||||
moment = Moment()
|
||||
db = SQLAlchemy()
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = 'auth.login'
|
||||
|
||||
|
||||
def create_app(config_name):
|
||||
@@ -21,9 +24,12 @@ 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
|
||||
from app.auth import auth as auth_blueprint
|
||||
|
||||
app.register_blueprint(main_blueprint)
|
||||
app.register_blueprint(auth_blueprint, url_prefix='/auth')
|
||||
|
||||
return app
|
||||
7
app/auth/__init__.py
Normal file
7
app/auth/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
auth = Blueprint('auth', __name__)
|
||||
|
||||
|
||||
from . import views
|
||||
29
app/auth/forms.py
Normal file
29
app/auth/forms.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField, ValidationError
|
||||
from wtforms.validators import DataRequired, Length, Email, EqualTo, Regexp
|
||||
|
||||
from app.models import User
|
||||
|
||||
|
||||
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')
|
||||
|
||||
|
||||
class RegistrationForm(FlaskForm):
|
||||
email = StringField('Email', validators=[DataRequired(), Length(1, 64), Email()])
|
||||
username = StringField('Username', validators=[DataRequired(), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0, 'Usernames must have only letters, numbers, dots or underscors')])
|
||||
password = PasswordField('Password', validators=[DataRequired(), EqualTo('password2', message='Passwords must match.')])
|
||||
password2 = PasswordField('Confirm Password', validators=[DataRequired()])
|
||||
submit = SubmitField('Register')
|
||||
|
||||
|
||||
def validate_email(self, field):
|
||||
if User.query.filter_by(email=field.data).first():
|
||||
raise ValidationError('Email already registered')
|
||||
|
||||
def validate_username(self, field):
|
||||
if User.query.filter_by(username=field.data).first():
|
||||
raise ValidationError('Username already in use')
|
||||
50
app/auth/views.py
Normal file
50
app/auth/views.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from flask import render_template, redirect, request, url_for, flash
|
||||
from flask_login import login_user, logout_user, login_required
|
||||
|
||||
from app import db
|
||||
from app.auth import auth
|
||||
from app.models import User
|
||||
from app.auth.forms import LoginForm, RegistrationForm
|
||||
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
next = request.args.get('next')
|
||||
|
||||
if next is None or next.startswith('/'):
|
||||
next = url_for('main.index')
|
||||
|
||||
return redirect(next)
|
||||
flash('Invalid username or password', category='error')
|
||||
return render_template('auth/login.html', form=form)
|
||||
|
||||
|
||||
@auth.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
flash('You have been logged out.', category='info')
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
|
||||
@auth.route('/register', methods=['get', 'post'])
|
||||
def register():
|
||||
form = RegistrationForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
user = User(email=form.email.data, username=form.username.data, password=form.password.data)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
flash('You can now login', category='success')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
return render_template('auth/register.html', form=form)
|
||||
@@ -1,4 +1,14 @@
|
||||
from app import db
|
||||
from flask import current_app
|
||||
from flask_login import UserMixin
|
||||
from itsdangerous import TimedJSONWebSignatureSerializer
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
from app import db, login_manager
|
||||
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id): return User.query.get(int(user_id))
|
||||
|
||||
|
||||
class Role(db.Model):
|
||||
@@ -11,14 +21,50 @@ class Role(db.Model):
|
||||
return f'<Role {self.name!r}>'
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
class User(UserMixin, db.Model):
|
||||
__tablename__ = 'users'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(64), unique=True, index=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))
|
||||
confirmed = db.Column(db.Boolean, default=False)
|
||||
|
||||
def __repr__(self): return f'<User {self.username!r}>'
|
||||
|
||||
@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, method='pbkdf2:sha256', salt_length=20)
|
||||
|
||||
def verify_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def generate_confirmation_token(self, expiration=3600):
|
||||
s = TimedJSONWebSignatureSerializer(current_app, expiration)
|
||||
return s.dumps({'confirm': self.id}).decode('utf-8')
|
||||
|
||||
def confirm(self, token):
|
||||
s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
|
||||
|
||||
try:
|
||||
data = s.loads(token.encode('utf-8'))
|
||||
except:
|
||||
return False
|
||||
|
||||
if data.get('confirm') != self.id:
|
||||
return False
|
||||
|
||||
self.confirmed = True
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ComicMeta(db.Model):
|
||||
__tablename__ = 'comicmeta'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -28,4 +74,4 @@ class ComicMeta(db.Model):
|
||||
previous_issue = db.Column(db.String(256), nullable=True)
|
||||
next_issue = db.Column(db.String(256), nullable=True)
|
||||
cover_path = db.Column(db.String(256))
|
||||
archive_path = db.Column(db.String(256))
|
||||
archive_path = db.Column(db.String(256))
|
||||
|
||||
19
app/templates/auth/login.html
Normal file
19
app/templates/auth/login.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Yo!nk - Login{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="page-header">
|
||||
<h1>Login</h1>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ form.email.label }} {{ form.email() }}
|
||||
{{ form.password.label }} {{ form.password() }}
|
||||
{{ form.remember_me.label }} {{ form.remember_me() }}
|
||||
{{ form.submit() }}
|
||||
</form>
|
||||
<p>New User? Click <a href="{{ url_for('auth.register') }}">here</a> to register a free account.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -30,6 +30,18 @@
|
||||
</button>
|
||||
|
||||
<a href="/" class="navbar-brand">Y!oink</a>
|
||||
|
||||
<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 class="navbar-collapse collapse">
|
||||
|
||||
@@ -25,7 +25,11 @@
|
||||
{{ form.download(class_='btn btn-outline-primary') }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<h2>{{ current_user.username }}'s Library</h2>
|
||||
{% else %}
|
||||
<h2>Comic Library</h2>
|
||||
{% endif %}
|
||||
{{ library.latest_downloads(latest) }}
|
||||
|
||||
{% endblock %}
|
||||
@@ -21,7 +21,7 @@ greenlet==1.1.2
|
||||
idna==3.3
|
||||
importlib-metadata==4.11.3
|
||||
iniconfig==1.1.1
|
||||
itsdangerous==2.1.2
|
||||
itsdangerous==2.0.1
|
||||
Jinja2==3.1.1
|
||||
Mako==1.2.0
|
||||
MarkupSafe==2.1.1
|
||||
|
||||
26
tests/test_model_user.py
Normal file
26
tests/test_model_user.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import unittest
|
||||
|
||||
from app.models import User
|
||||
|
||||
|
||||
|
||||
class UserModelTestCase(unittest.TestCase):
|
||||
|
||||
def test_password_setter(self):
|
||||
u = User(password='abcdefg')
|
||||
self.assertTrue(u.password_hash is not None)
|
||||
|
||||
def test_no_password_getter(self):
|
||||
u = User(password='abcdefg')
|
||||
with self.assertRaises(AttributeError):
|
||||
u.password
|
||||
|
||||
def test_password_verification(self):
|
||||
u = User(password='abcdefg')
|
||||
self.assertTrue(u.verify_password('abcdefg'))
|
||||
self.assertFalse(u.verify_password('password'))
|
||||
|
||||
def test_password_salts_are_random(self):
|
||||
u = User(password='abcdefg')
|
||||
u2 = User(password='abcdefg')
|
||||
self.assertTrue(u.password_hash != u2.password_hash)
|
||||
@@ -21,4 +21,4 @@ class BasicTestCase(unittest.TestCase):
|
||||
self.assertFalse(current_app is None)
|
||||
|
||||
def test_app_is_testing(self):
|
||||
self.assertTrue(current_app.config['TESTING'])
|
||||
self.assertTrue(current_app.config['TESTING'])
|
||||
|
||||
Reference in New Issue
Block a user