Browse Source

Add clean message flashing, error handling.

master
Ben Kurtovic 6 years ago
parent
commit
493de28a5d
8 changed files with 142 additions and 26 deletions
  1. +35
    -18
      app.py
  2. +8
    -0
      calefaction/auth.py
  3. +17
    -0
      calefaction/messages.py
  4. +42
    -7
      calefaction/util.py
  5. +20
    -0
      static/main.css
  6. +10
    -0
      templates/_base.mako
  7. +1
    -1
      templates/_default.mako
  8. +9
    -0
      templates/logout.mako

+ 35
- 18
app.py View File

@@ -3,7 +3,7 @@

from pathlib import Path

from flask import Flask, g, redirect, request, url_for
from flask import Flask, flash, g, redirect, request, url_for
from flask_mako import MakoTemplates, render_template

import calefaction
@@ -11,8 +11,10 @@ from calefaction.auth import AuthManager
from calefaction.config import Config
from calefaction.database import Database
from calefaction.eve import EVE
from calefaction.exceptions import AccessDeniedError, EVEAPIError
from calefaction.util import catch_errors, set_up_asset_versioning
from calefaction.messages import Messages
from calefaction.util import (
try_func, make_error_catcher, make_route_restricter,
set_up_asset_versioning)

app = Flask(__name__)

@@ -21,6 +23,9 @@ config = Config(basepath / "config")
Database.path = str(basepath / "data" / "db.sqlite3")
eve = EVE(config)
auth = AuthManager(config, eve)
catch_exceptions = make_error_catcher(app, "error.mako")
route_restricted = make_route_restricter(
auth, lambda: redirect(url_for("index"), 303))

MakoTemplates(app)
set_up_asset_versioning(app)
@@ -37,32 +42,44 @@ app.before_request(Database.pre_hook)
app.teardown_appcontext(Database.post_hook)

@app.route("/")
@catch_errors(app)
@catch_exceptions
def index():
... # handle flashed error messages in _base.mako
if auth.is_authenticated(): # ... need to check for exceptions
success, _ = try_func(auth.is_authenticated)
if success:
return render_template("home.mako")
return render_template("landing.mako")

@app.route("/login", methods=["GET", "POST"])
@catch_errors(app)
@catch_exceptions
def login():
code = request.args.get("code")
state = request.args.get("state")
try:
auth.handle_login(code, state)
except EVEAPIError:
... # flash error message
except AccessDeniedError:
... # flash error message
if getattr(g, "_session_expired"):
... # flash error message
success, caught = try_func(lambda: auth.handle_login(code, state))
if success:
flash(Messages.LOGGED_IN, "success")
elif getattr(g, "_session_expired", False):
flash(Messages.SESSION_EXPIRED, "error")
elif not caught:
flash(Messages.LOGIN_FAILED, "error")
return redirect(url_for("index"), 303)

# @app.route("/logout") ...
@app.route("/logout", methods=["GET", "POST"])
@catch_exceptions
def logout():
if request.method == "GET":
return render_template("logout.mako")

auth.handle_logout()
flash(Messages.LOGGED_OUT, "success")
return redirect(url_for("index"), 303)

# @auth.route_restricted ...
# check for same exceptions as login() and use same flashes
@app.route("/test")
@catch_exceptions
@route_restricted
def test():
...
return "Success! You are authenticated!"

if __name__ == "__main__":
app.run(debug=True, port=8080)

+ 8
- 0
calefaction/auth.py View File

@@ -238,3 +238,11 @@ class AuthManager:
g.db.attach_session(sid, char_id)
g.db.touch_session(sid)
return True

def handle_logout(self):
"""Log out the user if they are logged in.

Invalidates their session and clears the session cookie.
"""
self._invalidate_session()
session.clear()

+ 17
- 0
calefaction/messages.py View File

@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-

__all__ = ["Messages"]

class Messages:
"""Namespace for user interface message strings."""
# success
LOGGED_IN = "Logged in."
LOGGED_OUT = "Logged out."

# error
LOG_IN_FIRST = "You need to log in to access that page."
ACCESS_DENIED = "Your character is not permitted to access this site."
SESSION_EXPIRED = "Session expired. You need to log in again."
LOGIN_FAILED = "Login failed."
EVE_API_ERROR = ("There was an error communicating with EVE's servers. "
"Please wait a while and try again.")

+ 42
- 7
calefaction/util.py View File

@@ -5,12 +5,33 @@ from hashlib import md5
from os import path
from traceback import format_exc

from flask import url_for
from flask import flash, url_for
from flask_mako import render_template, TemplateError

__all__ = ["catch_errors", "set_up_asset_versioning"]
from .exceptions import AccessDeniedError, EVEAPIError
from .messages import Messages

def catch_errors(app):
__all__ = [
"try_func", "make_error_catcher", "make_route_restricter",
"set_up_asset_versioning"]

def try_func(inner):
"""Evaluate inner(), catching subclasses of CalefactionError.

If nothing was caught, return (inner(), False). Otherwise, flash an
appropriate error message and return (False, True).
"""
try:
result = inner()
return (result, False)
except EVEAPIError:
flash(Messages.EVE_API_ERROR, "error")
return (False, True)
except AccessDeniedError:
flash(Messages.ACCESS_DENIED, "error")
return (False, True)

def make_error_catcher(app, error_template):
"""Wrap a route to display and log any uncaught exceptions."""
def callback(func):
@wraps(func)
@@ -19,10 +40,24 @@ def catch_errors(app):
return func(*args, **kwargs)
except TemplateError as exc:
app.logger.error("Caught exception:\n{0}".format(exc.text))
return render_template("error.mako", traceback=exc.text)
return render_template(error_template, traceback=exc.text)
except Exception:
app.logger.exception("Caught exception:")
return render_template("error.mako", traceback=format_exc())
return render_template(error_template, traceback=format_exc())
return inner
return callback

def make_route_restricter(auth, on_failure):
"""Wrap a route to ensure the user is authenticated."""
def callback(func):
@wraps(func)
def inner(*args, **kwargs):
success, caught = try_func(auth.is_authenticated)
if success:
return func(*args, **kwargs)
if not caught:
flash(Messages.LOG_IN_FIRST, "error")
return on_failure()
return inner
return callback

@@ -40,8 +75,8 @@ def set_up_asset_versioning(app):
if cache and cache[0] == mtime:
hashstr = cache[1]
else:
with open(fpath, "rb") as f:
hashstr = md5(f.read()).hexdigest()
with open(fpath, "rb") as fp:
hashstr = md5(fp.read()).hexdigest()
app._hash_cache[fpath] = (mtime, hashstr)
return url_for("static", filename=filename, v=hashstr)
raise error


+ 20
- 0
static/main.css View File

@@ -149,3 +149,23 @@ footer ul li:not(:last-child):after {
#error pre {
white-space: pre-wrap;
}

#flashes {
margin-top: 0.5em;
}

#flashes > div {
padding: 0.5em 0.75em;
border-left-width: 4px;
border-left-style: solid;
}

#flashes > .success {
border-color: #33AA22;
background-color: rgba(60, 255, 30, 0.2);
}

#flashes > .error {
border-color: #AA3322;
background-color: rgba(255, 60, 30, 0.2);
}

+ 10
- 0
templates/_base.mako View File

@@ -38,6 +38,16 @@
<div id="container">
<div>
<main>
<%block name="flashes">
<% messages = get_flashed_messages(with_categories=True) %>
% if messages:
<div id="flashes">
% for category, message in messages:
<div class="${category | h}">${message | h}</div>
% endfor
</div>
% endif
</%block>
${next.body()}
</main>
</div>


+ 1
- 1
templates/_default.mako View File

@@ -6,6 +6,6 @@
</nav>
</%block>
<%block name="righthead">
PLAYER_NAME... [logout]
PLAYER_NAME... [logout] <!-- use GET /logout here and JS switch it to a POST form -->
</%block>
${next.body()}

+ 9
- 0
templates/logout.mako View File

@@ -0,0 +1,9 @@
<%inherit file="_base.mako"/>
<%block name="title">
Log out &ndash; ${g.config.get("corp.name") | h}
</%block>
<h1>Log out</h1> <!-- ... style -->
<p>Use the button below to safely log out and clear your session.</p>
<form method="post">
<input type="submit" value="Log out">
</form>

Loading…
Cancel
Save