mirror of
https://github.com/shouptech/humulus.git
synced 2026-02-03 13:49:41 +00:00
Add login capability
This commit is contained in:
parent
82c974fd8e
commit
195a007e85
10 changed files with 198 additions and 8 deletions
|
|
@ -42,4 +42,8 @@ def create_app(test_config=None):
|
|||
from . import recipes
|
||||
app.register_blueprint(recipes.bp)
|
||||
|
||||
# Register auth blueprint
|
||||
from . import auth
|
||||
app.register_blueprint(auth.bp)
|
||||
|
||||
return app
|
||||
|
|
|
|||
63
src/humulus/auth.py
Normal file
63
src/humulus/auth.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"""This module handles routes for authentication."""
|
||||
|
||||
# Copyright 2019 Mike Shoup
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import functools
|
||||
|
||||
from flask import (Blueprint, current_app, flash, redirect, render_template,
|
||||
session, url_for)
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import(PasswordField)
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
|
||||
bp = Blueprint('auth', __name__)
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
"""Form for login."""
|
||||
password = PasswordField('Password', validators=[DataRequired()])
|
||||
|
||||
|
||||
def login_required(view):
|
||||
"""View decorator that redirects anonymous users to the login page."""
|
||||
@functools.wraps(view)
|
||||
def wrapped_view(**kwargs):
|
||||
logged_in = session.get('logged_in', False)
|
||||
if not logged_in:
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
return view(**kwargs)
|
||||
|
||||
return wrapped_view
|
||||
|
||||
|
||||
@bp.route('/login', methods=('GET', 'POST'))
|
||||
def login():
|
||||
form = LoginForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.password.data == current_app.config['HUMULUS_PASSWORD']:
|
||||
session.clear()
|
||||
session['logged_in'] = True
|
||||
return redirect(url_for('index'))
|
||||
flash('Password is invalid.', category='warning')
|
||||
return render_template('auth/login.html', form=form)
|
||||
|
||||
|
||||
@bp.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect(url_for('index'))
|
||||
|
|
@ -22,6 +22,7 @@ from wtforms import (Form, StringField, DecimalField, TextAreaField, FieldList,
|
|||
FormField, SelectField)
|
||||
from wtforms.validators import DataRequired, Optional
|
||||
|
||||
from humulus.auth import login_required
|
||||
from humulus.couch import get_doc_or_404, put_doc, update_doc, get_view
|
||||
|
||||
bp = Blueprint('recipes', __name__, url_prefix='/recipes')
|
||||
|
|
@ -206,6 +207,7 @@ def index():
|
|||
|
||||
|
||||
@bp.route('/create', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
def create():
|
||||
form = RecipeForm()
|
||||
|
||||
|
|
@ -222,6 +224,7 @@ def info(id):
|
|||
|
||||
|
||||
@bp.route('/delete/<id>', methods=('POST',))
|
||||
@login_required
|
||||
def delete(id):
|
||||
recipe = get_doc_or_404(id)
|
||||
recipe.delete()
|
||||
|
|
@ -229,6 +232,7 @@ def delete(id):
|
|||
|
||||
|
||||
@bp.route('/update/<id>', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
def update(id):
|
||||
# Get the recipe from the database and validate it is the same revision
|
||||
form = RecipeForm()
|
||||
|
|
|
|||
|
|
@ -43,7 +43,11 @@
|
|||
</ul>
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="#">Login</a>
|
||||
{% if session.logged_in %}
|
||||
<a class="nav-link" href="{{ url_for('auth.logout') }}">Logout</a>
|
||||
{% else %}
|
||||
<a class="nav-link" href="{{ url_for('auth.login') }}">Login</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
30
src/humulus/templates/auth/login.html
Normal file
30
src/humulus/templates/auth/login.html
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{#-
|
||||
Copyright 2019 Mike Shoup
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-#}
|
||||
{% extends '_base.html' %}
|
||||
{% from "_macros.html" import render_field_with_errors %}
|
||||
{% block title %}Login{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<form method="POST" action_url="{{ url_for('auth.login') }}">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="row">
|
||||
{{ render_field_with_errors(form.password) }}
|
||||
</div>
|
||||
<div class="row">
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
@ -21,9 +21,11 @@
|
|||
|
||||
{% block body %}
|
||||
<div class="row"><h1>Recipes</h1></div>
|
||||
{% if session.logged_in %}
|
||||
<div class="row">
|
||||
<a href="{{ url_for('recipes.create') }}" class="btn btn-primary btn-sm mt-1 mb-2 ml-auto">Create a recipe</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
|
|
|
|||
|
|
@ -143,10 +143,12 @@
|
|||
{#-
|
||||
Buttons to do things
|
||||
-#}
|
||||
{% if session.logged_in %}
|
||||
<div class="row mt-4 pt-1 border-top">
|
||||
<a class="btn btn-secondary mr-1" href="{{ url_for('recipes.update', id=recipe._id) }}">Update Recipe</a>
|
||||
{{ render_delete_button('Delete Recipe', 'deleteRecipe', 'btn-danger') }}
|
||||
</div>
|
||||
{{ render_delete_modal(url_for('recipes.delete', id=recipe._id), 'deleteRecipe', recipe.name) }}
|
||||
{% endif %}
|
||||
<div class="row small mt-4">Recipe revision: {{ recipe._rev }}</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ def app():
|
|||
'COUCH_PASSWORD': 'password',
|
||||
'COUCH_DATABASE': dbname,
|
||||
'WTF_CSRF_ENABLED': False,
|
||||
'SECRET_KEY': 'testing'
|
||||
'SECRET_KEY': 'testing',
|
||||
'HUMULUS_PASSWORD': 'password'
|
||||
})
|
||||
|
||||
with app.app_context():
|
||||
|
|
@ -132,3 +133,22 @@ def runner(app):
|
|||
@pytest.fixture
|
||||
def client(app):
|
||||
return app.test_client()
|
||||
|
||||
|
||||
class AuthActions(object):
|
||||
def __init__(self, client):
|
||||
self._client = client
|
||||
|
||||
def login(self, password='password'):
|
||||
return self._client.post(
|
||||
'/login',
|
||||
data={'password': password}
|
||||
)
|
||||
|
||||
def logout(self):
|
||||
return self._client.get('/logout')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth(client):
|
||||
return AuthActions(client)
|
||||
|
|
|
|||
46
tests/test_auth.py
Normal file
46
tests/test_auth.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Copyright 2019 Mike Shoup
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from flask import session
|
||||
|
||||
|
||||
def test_login(client, auth):
|
||||
# Test GET
|
||||
response = client.get('/login')
|
||||
assert response.status_code == 200
|
||||
|
||||
# Test failed login
|
||||
data = {'password': 'invalid'}
|
||||
response = client.post('/login', data=data)
|
||||
assert response.status_code == 200
|
||||
assert b'Password is invalid' in response.data
|
||||
|
||||
# Test successful login
|
||||
data = {'password': 'password'}
|
||||
response = client.post('/login', data=data)
|
||||
assert response.status_code == 302
|
||||
with client.session_transaction() as session:
|
||||
assert session['logged_in']
|
||||
|
||||
|
||||
def test_logout(client, auth):
|
||||
# Login
|
||||
auth.login()
|
||||
with client.session_transaction() as session:
|
||||
assert session['logged_in']
|
||||
|
||||
response = client.get('/logout')
|
||||
assert response.status_code == 302
|
||||
with client.session_transaction() as session:
|
||||
assert not session.get('logged_in', False)
|
||||
|
|
@ -70,9 +70,14 @@ def test_index(client):
|
|||
)
|
||||
|
||||
|
||||
def test_create(client, app):
|
||||
def test_create(client, app, auth):
|
||||
"""Test success in creating a recipe document."""
|
||||
# Test GET
|
||||
# Test GET without login
|
||||
response = client.get('/recipes/create')
|
||||
assert response.status_code == 302
|
||||
|
||||
# Test GET with login
|
||||
auth.login()
|
||||
response = client.get('/recipes/create')
|
||||
assert response.status_code == 200
|
||||
|
||||
|
|
@ -95,8 +100,13 @@ def test_create(client, app):
|
|||
assert doc['efficiency'] == '65'
|
||||
|
||||
|
||||
def test_update(client, app):
|
||||
def test_update(client, app, auth):
|
||||
"""Test success in updating a recipe document."""
|
||||
# Test GET without login
|
||||
response = client.get('/recipes/update/awesome-lager')
|
||||
assert response.status_code == 302
|
||||
|
||||
auth.login()
|
||||
# Test GET on a bare minimum recipe
|
||||
response = client.get('/recipes/update/awesome-lager')
|
||||
assert response.status_code == 200
|
||||
|
|
@ -268,11 +278,16 @@ def test_recipe_form_doc(app):
|
|||
}
|
||||
|
||||
|
||||
def test_recipe_delete(client):
|
||||
def test_recipe_delete(client, auth):
|
||||
"""Test success in deleting a document."""
|
||||
# Try to delete a document without logging in
|
||||
response = client.post('/recipes/delete/awesome-lager')
|
||||
assert response.status_code == 302
|
||||
response = client.get('/recipes/info/awesome-lager')
|
||||
assert response.status_code == 200
|
||||
|
||||
# Try to get the doc now
|
||||
# Delete document after login
|
||||
auth.login()
|
||||
# Try to delete a document without logging in
|
||||
response = client.post('/recipes/delete/awesome-lager')
|
||||
response = client.get('/recipes/info/awesome-lager')
|
||||
assert response.status_code == 404
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue