diff --git a/.gitignore b/.gitignore index a1e953a..58db2e9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ config/* data/* !config/config.yml.sample +!data/schema.sql diff --git a/README.md b/README.md index c7f5100..e4fe31e 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ Guide ### Setup - cp config.yml.sample config.yml - vim config.yml + cp config/config.yml.sample config/config.yml + vim config/config.yml # follow instructions + cat data/schema.sql | sqlite3 data/db.sqlite3 ... ### Test diff --git a/app.py b/app.py index c05872e..13f1ced 100755 --- a/app.py +++ b/app.py @@ -5,30 +5,46 @@ from pathlib import Path from flask import Flask, g from flask_mako import MakoTemplates, render_template +from werkzeug.local import LocalProxy import calefaction +from calefaction.auth import AuthManager from calefaction.config import Config +from calefaction.database import Database from calefaction.eve import EVE from calefaction.util import catch_errors, set_up_hash_versioning -basepath = Path(__file__).resolve().parent app = Flask(__name__) + +basepath = Path(__file__).resolve().parent config = Config(basepath / "config") +Database.path = str(basepath / "data" / "db.sqlite3") eve = EVE() +auth = AuthManager(config, eve) MakoTemplates(app) set_up_hash_versioning(app) +config.install(app) @app.before_request def prepare_request(): + g.auth = auth g.config = config g.eve = eve g.version = calefaction.__version__ +app.before_request(Database.pre_hook) +app.teardown_appcontext(Database.post_hook) + @app.route("/") @catch_errors(app) def index(): return render_template("landing.mako") +@app.route("/login") +@catch_errors(app) +def login(): + return "login" # ... + if __name__ == "__main__": app.run(debug=True, port=8080) diff --git a/calefaction/auth.py b/calefaction/auth.py new file mode 100644 index 0000000..7a5ab5c --- /dev/null +++ b/calefaction/auth.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +from flask import g, session, url_for +from itsdangerous import URLSafeSerializer + +__all__ = ["AuthManager"] + +_SCOPES = ["publicData", "characterAssetsRead"] # ... + +class AuthManager: + + def __init__(self, config, eve): + self._config = config + self._eve = eve + + def _new_session_id(self): + with g.db as conn: + cur = conn.execute("INSERT INTO session DEFAULT VALUES") + return cur.lastrowid + + def get_session_id(self): + if "id" not in session: + session["id"] = self._new_session_id() + return session["id"] + + def get_state_hash(self): + key = self._config.get("auth.session_key") + serializer = URLSafeSerializer(key) + return serializer.dumps(self.get_session_id()) + + def make_login_link(self): + cid = self._config.get("auth.client_id") + target = url_for("login", _external=True, _scheme=self._config.scheme) + scopes = _SCOPES + state = self.get_state_hash() + return self._eve.sso.get_authorize_url(cid, target, scopes, state) diff --git a/calefaction/config.py b/calefaction/config.py index 599a128..31d0b49 100644 --- a/calefaction/config.py +++ b/calefaction/config.py @@ -22,3 +22,12 @@ class Config: return default obj = obj[item] return obj + + @property + def scheme(self): + return "https" if self.get("site.https") else "http" + + def install(self, app): + app.config["SERVER_NAME"] = self.get("site.canonical") + app.config["PREFERRED_URL_SCHEME"] = self.scheme + app.secret_key = self.get("auth.session_key") diff --git a/calefaction/database.py b/calefaction/database.py new file mode 100644 index 0000000..74f3a4e --- /dev/null +++ b/calefaction/database.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +import sqlite3 + +from flask import g +from werkzeug.local import LocalProxy + +__all__ = ["Database"] + +class Database: + path = None + + def __init__(self): + if self.path is None: + raise RuntimeError("Database.path not set") + self._conn = sqlite3.connect(self.path) + import traceback + + def __enter__(self): + return self._conn.__enter__() + + def __exit__(self, exc_type, exc_value, trace): + return self._conn.__exit__(exc_type, exc_value, trace) + + @classmethod + def _get(cls): + if not hasattr(g, "_db"): + g._db = cls() + return g._db + + @classmethod + def pre_hook(cls): + g.db = LocalProxy(cls._get) + + @classmethod + def post_hook(cls, exc): + if hasattr(g, "_db"): + g._db.close() + + def close(self): + return self._conn.close() diff --git a/calefaction/eve/__init__.py b/calefaction/eve/__init__.py index 948a19f..96b428e 100644 --- a/calefaction/eve/__init__.py +++ b/calefaction/eve/__init__.py @@ -2,6 +2,7 @@ from .clock import Clock from .image import ImageServer +from .sso import SSOManager __all__ = ["EVE"] @@ -10,6 +11,7 @@ class EVE: def __init__(self): self._clock = Clock() self._image = ImageServer() + self._sso = SSOManager() @property def clock(self): @@ -18,3 +20,7 @@ class EVE: @property def image(self): return self._image + + @property + def sso(self): + return self._sso diff --git a/calefaction/eve/clock.py b/calefaction/eve/clock.py index 883721f..1272c7e 100644 --- a/calefaction/eve/clock.py +++ b/calefaction/eve/clock.py @@ -4,10 +4,10 @@ from datetime import datetime __all__ = ["Clock"] -YEAR_DELTA = 1898 +_YEAR_DELTA = 1898 class Clock: def now(self): dt = datetime.utcnow() - return str(dt.year - YEAR_DELTA) + dt.strftime("-%m-%d %H:%M") + return str(dt.year - _YEAR_DELTA) + dt.strftime("-%m-%d %H:%M") diff --git a/calefaction/eve/sso.py b/calefaction/eve/sso.py new file mode 100644 index 0000000..88b8e15 --- /dev/null +++ b/calefaction/eve/sso.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +from urllib.parse import urlencode + +__all__ = ["SSOManager"] + +class SSOManager: + + def get_authorize_url(self, client_id, redirect_uri, scopes=None, + state=None): + baseurl = "https://login.eveonline.com/oauth/authorize?" + params = { + "response_type": "code", + "redirect_uri": redirect_uri, + "client_id": client_id + } + if scopes: + params["scope"] = " ".join(scopes) + if state is not None: + params["state"] = state + return baseurl + urlencode(params) diff --git a/config/config.yml.sample b/config/config.yml.sample index e8aad92..beebfe6 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -2,14 +2,37 @@ # Copy this to config.yml and modify it to set up your website. # You must restart the server after making any changes. +site: + # Full canonical server name; include port if not default: + canonical: example.com + # Assume HTTPS? This affects how URLs are generated, not how the site is + # served (setting up TLS is your responsibility): + https: yes + corp: - # Find your corp's ID at, e.g., https://zkillboard.com/corporation/917701062/ + # You need to reset the database if this value is changed in the future. + # Find your corp's ID at e.g. https://zkillboard.com/corporation/917701062/: id: 123456789 - # Full corp name (doesn't need to match in-game name exactly, but it should) + # Full corp name (doesn't need to match in-game name exactly, but it should): name: My Corp Name Here -# Default stylesheet from static/styles/*.css: -# one of "amarr", "caldari", "gallente", "minmatar", or add your own +auth: + # Secure session signing key. Never share with anyone. Can generate with + # "import base64, os; base64.b64encode(os.urandom(24))": + session_key: sEQMbNbxRxHBhyGtt8cuLEMN6sDM1JcP + # You need to create an application at + # https://developers.eveonline.com/applications for this corp's website. + # Set the callback URL to http(s):///login (match the protocol + # with "site.https" above) and the scopes to: + # - publicData + # - ... + # SSO client ID: + client_id: a290afea820b8dd8c46d3883898ab66d + # SSO client secret: + client_secret: XXAPGc0LM6wdOJAwSNQmliZ2QhQpoBuUutQY6Rlc + +# Default stylesheet from static/styles/*.css; +# one of "amarr", "caldari", "gallente", "minmatar", or create your own: style: null welcome: |- diff --git a/data/schema.sql b/data/schema.sql new file mode 100644 index 0000000..20fcd4e --- /dev/null +++ b/data/schema.sql @@ -0,0 +1,21 @@ +-- Schema for Calefaction's internal database + +DROP TABLE IF EXISTS session; + +CREATE TABLE session ( + session_id INTEGER PRIMARY KEY AUTOINCREMENT, + session_character INTEGER DEFAULT 0, + session_touched TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +DROP TABLE IF EXISTS character; + +CREATE TABLE character ( + character_id INTEGER PRIMARY KEY, + character_name TEXT, + character_token BLOB, + character_refresh BLOB, + character_token_expiry TIMESTAMP, + character_last_verify TIMESTAMP, + character_style TEXT +); diff --git a/static/main.css b/static/main.css index 445a700..f130e54 100644 --- a/static/main.css +++ b/static/main.css @@ -34,7 +34,7 @@ a:hover { } main, header, footer { - background-color: rgba(0, 0, 0, 0.75); + background-color: rgba(0, 0, 0, 0.8); border-color: #4A4A4A; } diff --git a/templates/_base.mako b/templates/_base.mako index d6335f5..281d3c2 100644 --- a/templates/_base.mako +++ b/templates/_base.mako @@ -6,6 +6,7 @@ <%block name="title">${g.config.get("corp.name") | h} + % if g.config.get("style"): <% stylesheet = "styles/{}.css".format(g.config.get("style")) %> @@ -21,10 +22,10 @@
diff --git a/templates/landing.mako b/templates/landing.mako index 2f6a6a2..48c80ba 100644 --- a/templates/landing.mako +++ b/templates/landing.mako @@ -1,6 +1,8 @@ <%inherit file="_base.mako"/> <%block name="righthead"> - + + Log in with EVE Online +
% for paragraph in g.config.get("welcome").split("\n\n"):