diasporadiaries

a platform for writing stories with personal accounts and messages
git clone git://parazyd.org/diasporadiaries.git
Log | Files | Refs | Submodules | README | LICENSE

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:
Mdiaspora.py | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mtemplates/dashboard.html | 6+++++-
Atemplates/fail_login.html | 15+++++++++++++++
Atemplates/fail_permissions.html | 15+++++++++++++++
Atemplates/login.html | 25+++++++++++++++++++++++++
Atemplates/logout.html | 17+++++++++++++++++
Mtemplates/nav.html | 27+++++++++++++++++++++++++++
Mtemplates/success_approve.html | 2+-
Atemplates/success_hide.html | 15+++++++++++++++
Mutils.py | 49+++++++++++++++++++++++++++++++++++++++++++++++++
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.