commit acb3d5a00300f2e97c009a211f91f3cac4dd7e14
parent 2fb42fb6b7f3d8b4c3f7a9cadf0dcabf62f285f9
Author: parazyd <parazyd@dyne.org>
Date: Sun, 20 Jan 2019 15:10:58 +0100
Add login/logout support via flask-login.
Diffstat:
10 files changed, 280 insertions(+), 8 deletions(-)
diff --git a/diaspora.py b/diaspora.py
@@ -22,18 +22,102 @@ from argparse import ArgumentParser
from random import choice
from time import time
-from flask import Markup, Flask, render_template, request
+from flask import Markup, Flask, render_template, request, url_for, redirect
+from flask_login import (LoginManager, UserMixin, login_required, login_user,
+ logout_user, current_user)
from db import (sql_delete_row_where, sql_update_row_where, sql_insert,
sql_select_col, sql_select_col_where)
from utils import (get_story, makenav, randomstring, getcountryname,
get_multiple_stories, get_multiple_stories_filtered,
- make_profile)
+ make_profile, find_user_by_id, find_user_by_email,
+ validate_user)
app = Flask(__name__)
+app.config['SECRET_KEY'] = 'super-secret'
app.add_template_global(makenav)
+login_manager = LoginManager()
+login_manager.init_app(app)
+login_manager.login_view = 'login'
+
+
+class LoginUser(UserMixin):
+ """
+ Class for a user
+ """
+ def __init__(self, uid):
+ """ Init """
+ self.uid = uid
+
+ @property
+ def username(self):
+ """ Username """
+ user = self.get_user()
+ return user['email']
+
+ @property
+ def is_admin(self):
+ """ Check capabilities """
+ user = self.get_user()
+ if user['cap'] == 2:
+ return True
+ return False
+
+ def get_user(self):
+ """ Get user struct """
+ return find_user_by_id(self.uid)
+
+
+@login_manager.user_loader
+def load_user(user_id):
+ """
+ flask-security function for loading the LoginUser class for a user.
+ """
+ user = find_user_by_id(user_id)
+ if not user:
+ return None
+ return LoginUser(uid=user_id)
+
+
+@app.route('/login', methods=['GET', 'POST'])
+def login():
+ """
+ User login route.
+ """
+ if request.method == 'POST':
+ if request.form['42']:
+ return render_template('fail_login.html', msg='You robot!')
+
+ username = request.form['username']
+ password = request.form['password']
+
+ user = find_user_by_email(username)
+ if not user:
+ return render_template('fail_login.html', msg='No such user.')
+
+ if validate_user(user, password):
+ login_user(LoginUser(user['id']), remember=True)
+ nxt = request.args.get('next')
+ if not nxt:
+ nxt = url_for('main')
+ return redirect(nxt)
+ else:
+ return render_template('fail_login.html', msg='Incorrect password.')
+
+ return render_template('login.html')
+
+
+@app.route('/logout')
+@login_required
+def logout():
+ """
+ User logout route.
+ """
+ logout_user()
+ return render_template('logout.html')
+
@app.route('/write', methods=['GET', 'POST'])
def write():
@@ -70,10 +154,14 @@ def write():
@app.route('/edit', methods=['GET', 'POST'])
+@login_required
def edit():
"""
Route for editing and redacting.
"""
+ if not current_user.is_admin:
+ return render_template('fail_permissions.html')
+
if request.method != 'POST':
story = get_story(request.args.get('id'))
if story:
@@ -107,10 +195,14 @@ def view():
@app.route('/delete')
+@login_required
def delete():
"""
Route for deleting a specific story.
"""
+ if not current_user.is_admin:
+ return render_template('fail_permissions.html')
+
delkey = request.args.get('key')
if delkey:
story_id = sql_select_col_where('id', 'deletekey', delkey)
@@ -174,17 +266,30 @@ def profile():
@app.route('/dashboard')
+@login_required
def dashboard():
"""
Route for dashboard/admin view.
"""
- story_id = request.args.get('approveid')
- if story_id:
- sql_update_row_where([('visible', 1)], 'id', story_id)
+ if not current_user.is_admin:
+ return render_template('fail_permissions.html')
+
+ fparam = {'pending': 0, 'posted': 1}
+
+
+ add_id = request.args.get('approveid')
+ if add_id:
+ sql_update_row_where([('visible', 1)], 'id', add_id)
return render_template('success_approve.html')
- stories = get_multiple_stories('visible', 0)
+ hide_id = request.args.get('hideid')
+ if hide_id:
+ sql_update_row_where([('visible', 0)], 'id', hide_id)
+ return render_template('success_hide.html')
+
+ act = request.args.get('list', 'pending')
+ stories = get_multiple_stories('visible', fparam[act])
return render_template('dashboard.html', stories=stories)
diff --git a/templates/dashboard.html b/templates/dashboard.html
@@ -12,7 +12,7 @@
<div class="container">
{% if not stories %}
- <p>No pending stories in queue.</p>
+ <p>No stories to list.</p>
{% endif %}
{% for i in stories %}
@@ -29,7 +29,11 @@
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<a href="/edit?id={{ i['id'] }}" class="btn btn-sm btn-outline-secondary">Edit</a>
+ {% if i['visible'] == 0 %}
<a href="/dashboard?approveid={{ i['id'] }}" class="btn btn-sm btn-outline-secondary">Add</a>
+ {% else %}
+ <a href="/dashboard?hideid={{ i['id'] }}" class="btn btn-sm btn-outline-secondary">Hide</a>
+ {% endif %}
</div>
<img style="height: 1em;" src="/static/img/flags/{{ i['embark'] }}.png" alt="{{ i['embarkname'] }}" title="{{ i['embarkname' ]}}"><span class="fa fa-anchor"></span><img style="height: 1em;" src="/static/img/flags/{{ i['disembark'] }}.png" alt="{{ i['disembarkname'] }}" title="{{ i['disembarkname'] }}">
diff --git a/templates/fail_login.html b/templates/fail_login.html
@@ -0,0 +1,15 @@
+{% include 'header.html' %}
+
+ <title>Login fail | Diaspora Diaries</title>
+
+{% include 'nav.html' %}
+
+ <main role="main" class="container cover">
+
+ <h1 class="cover-heading">Error!</h1>
+
+ <p class="lead">{{ msg }}</p>
+
+ </main>
+
+{% include 'footer.html' %}
diff --git a/templates/fail_permissions.html b/templates/fail_permissions.html
@@ -0,0 +1,15 @@
+{% include 'header.html' %}
+
+ <title>Access denied | Diaspora Diaries</title>
+
+{% include 'nav.html' %}
+
+ <main role="main" class="container cover">
+
+ <h1 class="cover-heading">Error!</h1>
+
+ <p class="lead">You don't have the required permissions to view this page.</p>
+
+ </main>
+
+{% include 'footer.html' %}
diff --git a/templates/login.html b/templates/login.html
@@ -0,0 +1,25 @@
+{% include 'header.html' %}
+
+ <title>Login | Diaspora Diaries</title>
+
+{% include 'nav.html' %}
+
+ <main role="main" class="container">
+
+ <form action="/login" method="POST">
+ <p class="lead">Email:<br>
+ <input type="text" name="username" placeholder="email" required>
+ </p>
+
+ <p class="lead">Password:<br>
+ <input type="text" name="password" placeholder="password" required>
+ </p>
+
+ <input type="text" name="42" placeholder="Yeah sure" style="display: none;">
+
+ <input type="submit" value="Login">
+ </form>
+
+ </main>
+
+ {% include 'footer.html' %}
diff --git a/templates/logout.html b/templates/logout.html
@@ -0,0 +1,17 @@
+{% include 'header.html' %}
+
+ <title>Logout | Diaspora Diaries</title>
+
+{% include 'nav.html' %}
+
+ <main role="main" class="container cover">
+
+ <h1 class="cover-heading">Success!</h1>
+
+ <p class="lead">Successfully logged out!</p>
+
+ <p class="lead">You can return to the <a href="/">homepage</a> now.</p>
+
+ </main>
+
+{% include 'footer.html' %}
diff --git a/templates/nav.html b/templates/nav.html
@@ -34,5 +34,32 @@
<a class="nav-link disabled" href="/contact">Contact</a>
</li>
</ul>
+
+ <ul class="navbar-nav ml-auto">
+ {% if current_user.is_active %}
+ {% if current_user.is_admin %}
+ <li class="nav-item dropdown">
+ <a class="nav-link dropdown-toggle" href="#" id="navbarDashboard" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ Dashboard
+ </a>
+ <div class="dropdown-menu" aria-labelledby="navbarDashboard">
+ <a class="dropdown-item" href="/dashboard?list=pending">
+ <span class="fa fa-envelope"></span> Pending stories
+ </a>
+ <a class="dropdown-item" href="/dashboard?list=posted">
+ <span class="fa fa-envelope-open"></span> Posted stories
+ </a>
+ </div>
+ </li>
+ {% endif %}
+ <li class="nav-item">
+ <a class="nav-link" href="/logout">Logout</a>
+ </li>
+ {% else %}
+ <li class="nav-item">
+ <a class="nav-link" href="/login">Login</a>
+ </li>
+ {% endif %}
+ </ul>
</div>
</nav>
diff --git a/templates/success_approve.html b/templates/success_approve.html
@@ -8,7 +8,7 @@
<h1 class="cover-heading">Story approved!</h1>
- <p class="lead">You can return to the <a href="/dashboard">dashboard</a> now.</p>
+ <p class="lead">You can return to the <a href="/dashboard?list=pending">dashboard</a> now.</p>
</main>
diff --git a/templates/success_hide.html b/templates/success_hide.html
@@ -0,0 +1,15 @@
+{% include 'header.html' %}
+
+ <title>Hidden | Diaspora Diaries</title>
+
+{% include 'nav.html' %}
+
+ <main role="main" class="container cover">
+
+ <h1 class="cover-heading">Story hidden.</h1>
+
+ <p class="lead">You can return to the <a href="/dashboard?list=posted">dashboard</a> now.</p>
+
+ </main>
+
+{% include 'footer.html' %}
diff --git a/utils.py b/utils.py
@@ -136,6 +136,55 @@ def makenav(randomize=False):
return navlist
+def fill_user_dict(row):
+ """
+ Function to fill a dict with the given user's row from the database.
+ """
+ return {
+ 'id': row[0],
+ 'email': row[1],
+ 'name': row[2],
+ 'password': row[3],
+ 'cap': row[4],
+ 'first_seen': row[5],
+ 'is_active': row[6],
+ 'is_authed': row[7],
+ }
+
+
+def find_user_by_id(user_id):
+ """
+ Queries the database for a specific user id and returns the user dict.
+ """
+ row = sql_select_col_where('*', 'id', user_id, table='users')
+ if not row:
+ return None
+
+ return fill_user_dict(row[0])
+
+
+def find_user_by_email(email):
+ """
+ Queries the database for a user's email (username) and returns the user
+ dict.
+ """
+ row = sql_select_col_where('*', 'email', email, table='users')
+ if not row:
+ return None
+
+ return fill_user_dict(row[0])
+
+
+def validate_user(user, pw):
+ """
+ Validates a user login.
+ """
+ if hashpw(pw.encode(), user['password']) == user['password']:
+ return True
+
+ return False
+
+
def make_profile(name, email):
"""
Helper function to generate and insert a profile into the database.