@@ -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) |
@@ -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() |
@@ -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.") |
@@ -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 | ||||
@@ -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); | |||||
} |
@@ -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> | ||||
@@ -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()} |
@@ -0,0 +1,9 @@ | |||||
<%inherit file="_base.mako"/> | |||||
<%block name="title"> | |||||
Log out – ${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> |