Browse Source

Add clean message flashing, error handling.

master
Ben Kurtovic 8 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 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 from flask_mako import MakoTemplates, render_template


import calefaction import calefaction
@@ -11,8 +11,10 @@ from calefaction.auth import AuthManager
from calefaction.config import Config from calefaction.config import Config
from calefaction.database import Database from calefaction.database import Database
from calefaction.eve import EVE 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__) app = Flask(__name__)


@@ -21,6 +23,9 @@ config = Config(basepath / "config")
Database.path = str(basepath / "data" / "db.sqlite3") Database.path = str(basepath / "data" / "db.sqlite3")
eve = EVE(config) eve = EVE(config)
auth = AuthManager(config, eve) 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) MakoTemplates(app)
set_up_asset_versioning(app) set_up_asset_versioning(app)
@@ -37,32 +42,44 @@ app.before_request(Database.pre_hook)
app.teardown_appcontext(Database.post_hook) app.teardown_appcontext(Database.post_hook)


@app.route("/") @app.route("/")
@catch_errors(app)
@catch_exceptions
def index(): 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("home.mako")
return render_template("landing.mako") return render_template("landing.mako")


@app.route("/login", methods=["GET", "POST"]) @app.route("/login", methods=["GET", "POST"])
@catch_errors(app)
@catch_exceptions
def login(): def login():
code = request.args.get("code") code = request.args.get("code")
state = request.args.get("state") 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) 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__": if __name__ == "__main__":
app.run(debug=True, port=8080) 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.attach_session(sid, char_id)
g.db.touch_session(sid) g.db.touch_session(sid)
return True 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 os import path
from traceback import format_exc from traceback import format_exc


from flask import url_for
from flask import flash, url_for
from flask_mako import render_template, TemplateError 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.""" """Wrap a route to display and log any uncaught exceptions."""
def callback(func): def callback(func):
@wraps(func) @wraps(func)
@@ -19,10 +40,24 @@ def catch_errors(app):
return func(*args, **kwargs) return func(*args, **kwargs)
except TemplateError as exc: except TemplateError as exc:
app.logger.error("Caught exception:\n{0}".format(exc.text)) 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: except Exception:
app.logger.exception("Caught 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 inner
return callback return callback


@@ -40,8 +75,8 @@ def set_up_asset_versioning(app):
if cache and cache[0] == mtime: if cache and cache[0] == mtime:
hashstr = cache[1] hashstr = cache[1]
else: 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) app._hash_cache[fpath] = (mtime, hashstr)
return url_for("static", filename=filename, v=hashstr) return url_for("static", filename=filename, v=hashstr)
raise error raise error


+ 20
- 0
static/main.css View File

@@ -149,3 +149,23 @@ footer ul li:not(:last-child):after {
#error pre { #error pre {
white-space: pre-wrap; 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 id="container">
<div> <div>
<main> <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()} ${next.body()}
</main> </main>
</div> </div>


+ 1
- 1
templates/_default.mako View File

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