From f1131bfc8cc3e80900eb349d656e269d7682d2ae Mon Sep 17 00:00:00 2001 From: Linnnus Date: Sat, 27 Apr 2024 20:20:31 +0200 Subject: fix: Add missing dependencies Previous commits rely on added dependencies, but these were not added to the list of requirements. The dependencies in question: - bottle_sqlite - python-dotenv fixup: 110e05bad2c378473954e41231d7581754c8cc8f fixup: 0cb2a367968ea0bc45739da5c88fd7b88ca281a7 --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 1544077..06829c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ bottle==0.12.25 +bottle-sqlite==0.2.0 certifi==2024.2.2 charset-normalizer==3.3.2 gunicorn==21.2.0 @@ -7,6 +8,7 @@ Jinja2==3.1.3 MarkupSafe==2.1.5 oauthlib==3.2.2 packaging==24.0 +python-dotenv==1.0.1 requests==2.31.0 requests-oauthlib==2.0.0 urllib3==2.2.1 -- cgit v1.2.3 From ed0ed51c99da86882b5e79ec973116e0d833ee78 Mon Sep 17 00:00:00 2001 From: Linnnus Date: Sat, 27 Apr 2024 20:23:14 +0200 Subject: fix: Ignore database file As of 110e05bad2c378473954e41231d7581754c8cc8f we create a database file in PWD when running. This should obviously not be checked into version control. --- .gitignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f6e7164..0f2d2dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ +# Secrets .env + # Cert files cert.pem -key.pem \ No newline at end of file +key.pem + +# Database +thisisadatabasethatcontainsdata.db -- cgit v1.2.3 From 2bc4e69a4b08dbbd60b1ed711d6cfe825adb0209 Mon Sep 17 00:00:00 2001 From: Linnnus Date: Sat, 27 Apr 2024 20:25:43 +0200 Subject: Switch to gevent as backing WSGI server We've switched the backing server a few times: At 45a7c91fbef2e9c2c0c6821edb06bae75077b50c Linnnus added gunicorn because it supported SSL certificates, unlike the default server. At 0cb2a367968ea0bc45739da5c88fd7b88ca281a7 Jannick switched to cherrypy. I'm not sure why. At 22d80f90b6b60b6a40a30b772716b950239b539b Jannick switched to switched to Waitress, which worked on Windows (unlike gunicorn) but doesn't support SSL. Now, I'm switching to gevent which supports SSL and (apparently) windows. Hopefully we won't have to switch again. --- app.py | 3 ++- requirements.txt | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index 5529326..c64c280 100644 --- a/app.py +++ b/app.py @@ -1,3 +1,4 @@ +from gevent import monkey; monkey.patch_all() # MUST BE FIRST IMPORT from bottle import Bottle, run, debug, static_file, request, redirect, response, HTTPError from bottle import jinja2_template as template from oauthlib.oauth2 import WebApplicationClient @@ -69,4 +70,4 @@ def server_static(type, filename): debug(True) run(app, host='localhost', port=8080, reloader=True, - server="waitress", keyfile="./pki/server.key", certfile="./pki/server.crt") + server="gevent", keyfile="./pki/server.key", certfile="./pki/server.crt") diff --git a/requirements.txt b/requirements.txt index 06829c0..0fa0176 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,8 @@ bottle==0.12.25 bottle-sqlite==0.2.0 certifi==2024.2.2 charset-normalizer==3.3.2 -gunicorn==21.2.0 +gevent==24.2.1 +greenlet==3.0.3 idna==3.7 Jinja2==3.1.3 MarkupSafe==2.1.5 @@ -12,3 +13,5 @@ python-dotenv==1.0.1 requests==2.31.0 requests-oauthlib==2.0.0 urllib3==2.2.1 +zope.event==5.0 +zope.interface==6.3 -- cgit v1.2.3 From c0d2b9eb7e2b65b582039aafdca765fe32acf81e Mon Sep 17 00:00:00 2001 From: Linnnus Date: Sat, 27 Apr 2024 21:21:51 +0200 Subject: Flesh out 'join' user flow This commit splits the user flow when sending in an application to join the guild into three staged: 1. Intro text 2. HTML form 3. Form submission feedback (aka. "yay it went through") --- app.py | 12 ++++++--- static/styles/base.css | 11 ++++++++ static/styles/join.css | 54 ---------------------------------------- static/styles/join_common.css | 44 ++++++++++++++++++++++++++++++++ static/styles/join_form.css | 33 ++++++++++++++++++++++++ views/base.html | 2 +- views/join.html | 57 ------------------------------------------ views/join_form.html | 58 +++++++++++++++++++++++++++++++++++++++++++ views/join_intro.html | 23 +++++++++++++++++ views/join_success.html | 18 ++++++++++++++ 10 files changed, 197 insertions(+), 115 deletions(-) delete mode 100644 static/styles/join.css create mode 100644 static/styles/join_common.css create mode 100644 static/styles/join_form.css delete mode 100644 views/join.html create mode 100644 views/join_form.html create mode 100644 views/join_intro.html create mode 100644 views/join_success.html diff --git a/app.py b/app.py index c64c280..de7a1d3 100644 --- a/app.py +++ b/app.py @@ -43,11 +43,15 @@ def callback(): return f'Access token: {token_response.get("access_token")}' -@app.route("/join.html") +@app.route("/join_intro.html") +def join_intro(): + return template("join_intro") + +@app.route("/join_form.html") def join_form(): - return template("join") + return template("join_form") -@app.route("/join.html", method="POST") +@app.route("/join_form.html", method="POST") def join_submission(db): name = request.forms.get("name") preferred_role = request.forms.get("preferredRole") @@ -64,6 +68,8 @@ def join_submission(db): db.execute(f"INSERT INTO applications(name, role, motivation) VALUES ({name}, {preferred_role}, {motivation})") + return template("join_success") + @app.route("//") def server_static(type, filename): return static_file(filename, root=f"./static/{type}/") diff --git a/static/styles/base.css b/static/styles/base.css index f9b1ae6..2ae562e 100644 --- a/static/styles/base.css +++ b/static/styles/base.css @@ -60,3 +60,14 @@ footer { font-size: small; margin: 1rem; } + +/* Chill link styling */ +a { + /* dark and twisted color, lighter for contrast */ + color: #a27d7d; + text-decoration: underline; +} + +a:visited { + color: inherit; +} diff --git a/static/styles/join.css b/static/styles/join.css deleted file mode 100644 index f958aad..0000000 --- a/static/styles/join.css +++ /dev/null @@ -1,54 +0,0 @@ -/* - * This file contains styles specific to the "join" view. The corresponding - * HTML/template file is `views/join.html`. - */ - -main { - width: 100%; - max-width: 500px; - margin: 2rem auto; -} - -.signup { - -} - -.signup__box { - margin-bottom: 1reM; -} - -.signup__label { - font-weight: bold; -} - -.signup__input { - width: 100%; - padding: .5rem; - border-radius: 5px; -} - -textarea.signup__input { - /* Remove UA's default monospace font */ - font-family: inherit; - - /* Horizontal resizing totally breaks our layout :( */ - resize: vertical; -} - -.signup__submit { - /* Horizontally center the element */ - display: block; - margin-inline: auto; - - /* Make it look dark and twisted */ - background-color: #6e1818; - color: white; - border: none; - border-radius: 500px; - padding: 1rem; - font-size: large; -} - -/* The usual dimming effect makes interaction feel more tactile */ -.signup__submit:hover { filter: brightness(90%); } -.signup__submit:active { filter: brightness(60%); } \ No newline at end of file diff --git a/static/styles/join_common.css b/static/styles/join_common.css new file mode 100644 index 0000000..93bdb1d --- /dev/null +++ b/static/styles/join_common.css @@ -0,0 +1,44 @@ +/* + * This file contains styles specific to the "join" views. There are a couple + * of views which are all part of the same user flow and reuse some styles. The + * corresponding HTML/template files are `views/join_*.html`. + */ + +main { + /* Center align and keep width readable. */ + width: 100%; + max-width: 500px; + margin: 2rem auto; + +} + +/* Some pages are text heavy. Keep a nice line distance. */ +p { line-height: 1.5; } + +.button { + /* Horizontally center the element */ + display: block; + margin-inline: auto; + + /* Make it look dark and twisted */ + background-color: #6e1818; + color: white; + border: none; + border-radius: 500px; + padding: 1rem; + font-size: large; +} + +/* The usual dimming effect makes interaction feel more tactile */ +.button:hover { filter: brightness(90%); } +.button:active { filter: brightness(60%); } + +/* We use button-styles anchor-tags to preserve HTML semantics. */ +a.button { + /* Remove link styling */ + color: inherit; + text-decoration: none; + + /* Inline elements expand by default. */ + width: fit-content; +} diff --git a/static/styles/join_form.css b/static/styles/join_form.css new file mode 100644 index 0000000..f4ba0f1 --- /dev/null +++ b/static/styles/join_form.css @@ -0,0 +1,33 @@ +/* + * This file contains styles specific to the "join_form" view. The corresponding + * HTML/template file is `views/join_form.html`. + * + * See also the file `static/styles/join_common.css` which contains some styles + * which are reused among the "join_" group of views. + */ + +.signup { + +} + +.signup__box { + margin-bottom: 1reM; +} + +.signup__label { + font-weight: bold; +} + +.signup__input { + width: 100%; + padding: .5rem; + border-radius: 5px; +} + +textarea.signup__input { + /* Remove UA's default monospace font */ + font-family: inherit; + + /* Horizontal resizing totally breaks our layout :( */ + resize: vertical; +} diff --git a/views/base.html b/views/base.html index 9133090..cba0c5d 100644 --- a/views/base.html +++ b/views/base.html @@ -14,7 +14,7 @@ diff --git a/views/join.html b/views/join.html deleted file mode 100644 index 784019c..0000000 --- a/views/join.html +++ /dev/null @@ -1,57 +0,0 @@ -{% extends "base.html" %} - -{% block head %} - - -{% endblock %} - -{% block content %} - -{% endblock %} \ No newline at end of file diff --git a/views/join_form.html b/views/join_form.html new file mode 100644 index 0000000..0e06399 --- /dev/null +++ b/views/join_form.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% block head %} + + + +{% endblock %} + +{% block content %} + +{% endblock %} diff --git a/views/join_intro.html b/views/join_intro.html new file mode 100644 index 0000000..9ed93d5 --- /dev/null +++ b/views/join_intro.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block head %} + +{% endblock %} + +{% block content %} +

Joining the Blind Guild

+

+ So you want to join the Blind Guild. + Be warned, + we are a very exclusive club + and we aren't actively seeking new members. + However, if you're a skilled player + and feel like you could help elevate the Blind Guild, + feel free to send us an application! +

+

+ Click the button below to go to the form + where you can submit your application. +

+ Apply +{% endblock %} diff --git a/views/join_success.html b/views/join_success.html new file mode 100644 index 0000000..7e9dace --- /dev/null +++ b/views/join_success.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block head %} + +{% endblock %} + +{% block content %} +

Application recieved

+

+ Your application was submitted successfully! + We'll review it and decide if you're cool enough to join our exclusive club. + Please note that it may take a few days for us to get to your application. +

+

+ If you're accepted into our elite gang of high-tier gamers, + you'll receive an in-game invite to the Blind Guild. +

+{% endblock %} -- cgit v1.2.3 From b8825bf532dcbca86d07cfa7b57523051afd6a24 Mon Sep 17 00:00:00 2001 From: Linnnus Date: Sat, 27 Apr 2024 21:27:28 +0200 Subject: Save applications in database A broken statement was introduced in 2bf130581b763819672551c138cc70119005ef93. This patch properly initializes the database and prevents SQL injection attacks. --- app.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index de7a1d3..4e40ace 100644 --- a/app.py +++ b/app.py @@ -18,8 +18,22 @@ AUTH_BASE_URL = 'https://oauth.battle.net/authorize' TOKEN_URL = "https://oauth.battle.net/token" client = WebApplicationClient(CLIENT_ID) +DB_PATH = "thisisadatabasethatcontainsdata.db" + +connection = sqlite3.connect(DB_PATH) +cursor = connection.cursor() +cursor.executescript(""" + CREATE TABLE IF NOT EXISTS applications ( + username VARCHAR(12) NOT NULL, + preferredRole VARCHAR(6) NOT NULL, + motivation TEXT NOT NULL + ); +""") +cursor.close() +connection.close() + app = Bottle() -plugin = sqlite.Plugin(dbfile="thisisadatabasethatcontainsdata.db") +plugin = sqlite.Plugin(dbfile=DB_PATH) app.install(plugin) @app.route("/") @@ -52,7 +66,7 @@ def join_form(): return template("join_form") @app.route("/join_form.html", method="POST") -def join_submission(db): +def join_submission(db: sqlite3.Connection): name = request.forms.get("name") preferred_role = request.forms.get("preferredRole") motivation = request.forms.get("motivation") @@ -66,7 +80,7 @@ def join_submission(db): if motivation == None or motivation.strip() == "": raise HTTPError(400, "Motivitaion field is empty or missing.") - db.execute(f"INSERT INTO applications(name, role, motivation) VALUES ({name}, {preferred_role}, {motivation})") + db.execute(f"INSERT INTO applications(username, preferredRole, motivation) VALUES (?, ?, ?)", (name, preferred_role, motivation)) return template("join_success") -- cgit v1.2.3 From 3a33268067897b8670d0d39f5fd93e499230fd63 Mon Sep 17 00:00:00 2001 From: Linnnus Date: Sun, 28 Apr 2024 11:56:37 +0200 Subject: fix: Don't use CLIENT_ID as CLIENT_SECRET --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 4e40ace..2398f41 100644 --- a/app.py +++ b/app.py @@ -12,7 +12,7 @@ from bottle.ext import sqlite load_dotenv() CLIENT_ID = os.environ.get("CLIENT_ID") # DOTENV ligger paa discorden, repoet er publkic saa det -CLIENT_SECRET = os.environ.get("CLIENT_ID") # DOTENV PAHAHAH +CLIENT_SECRET = os.environ.get("CLIENT_SECRET") # DOTENV PAHAHAH REDIRECT_URI = "https://localhost:8080/callback" AUTH_BASE_URL = 'https://oauth.battle.net/authorize' TOKEN_URL = "https://oauth.battle.net/token" -- cgit v1.2.3 From 4ea85a09f7ab6932472ecfcf43c82515928df477 Mon Sep 17 00:00:00 2001 From: Linnnus Date: Mon, 29 Apr 2024 09:33:20 +0200 Subject: Use oath userid to identify applicants --- app.py | 43 ++++++++++++++++++++++++++++--------------- views/base.html | 1 - views/join_form.html | 1 + views/join_intro.html | 7 ++++--- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/app.py b/app.py index 2398f41..73926e3 100644 --- a/app.py +++ b/app.py @@ -26,7 +26,8 @@ cursor.executescript(""" CREATE TABLE IF NOT EXISTS applications ( username VARCHAR(12) NOT NULL, preferredRole VARCHAR(6) NOT NULL, - motivation TEXT NOT NULL + motivation TEXT NOT NULL, + userId INTEGER NOT NULL ); """) cursor.close() @@ -41,6 +42,10 @@ app.install(plugin) def index(): return template("index") +@app.route("/join_intro.html") +def join_intro(): + return template("join_intro") + @app.route("/battle") def battle(): state = secrets.token_urlsafe(16) @@ -49,27 +54,32 @@ def battle(): return redirect(authorization_url) @app.route('/callback') -def callback(): +def join_form(): state = request.get_cookie('oauth_state') - code = request.query.get('code') oauth2_session = OAuth2Session(CLIENT_ID, state=state, redirect_uri=REDIRECT_URI) token_response = oauth2_session.fetch_token(TOKEN_URL, authorization_response=request.url, client_secret=CLIENT_SECRET) - return f'Access token: {token_response.get("access_token")}' - -@app.route("/join_intro.html") -def join_intro(): - return template("join_intro") - -@app.route("/join_form.html") -def join_form(): - return template("join_form") - -@app.route("/join_form.html", method="POST") + # Get the user ID of the just authenticated user. As per the API + # documentation, this should be used to identify users. + # + # See: https://develop.battle.net/documentation/guides/regionality-and-apis#:~:text=Developers%20should%20use%20an%20accountId + query_parameters = { + "region": "eu", + } + response = oauth2_session.get("https://oauth.battle.net/oauth/userinfo", params=query_parameters) + response.raise_for_status() + user_info = response.json() + user_id = user_info["id"] + + # We pass the token retrieved here so it can be submitted with the rest of the application. + return template("join_form", user_id=user_id) + +@app.route("/callback", method="POST") def join_submission(db: sqlite3.Connection): name = request.forms.get("name") preferred_role = request.forms.get("preferredRole") motivation = request.forms.get("motivation") + user_id = request.forms.get("userId") if name == None or name.strip() == "": raise HTTPError(400, "Namefield is empty or missing. ( warning: this is not good )") @@ -79,8 +89,11 @@ def join_submission(db: sqlite3.Connection): raise HTTPError(400, "Preferred role must be one of the options (DPS, Tank, Healer) ( idiot )") if motivation == None or motivation.strip() == "": raise HTTPError(400, "Motivitaion field is empty or missing.") + if user_id == None or not user_id.isdigit(): + raise HTTPError(400, "Missing or invalid user id") - db.execute(f"INSERT INTO applications(username, preferredRole, motivation) VALUES (?, ?, ?)", (name, preferred_role, motivation)) + # FIXME: The user id is a 64-bit unsigned integer which may be larger than the INTEGER type of sqlite3. + db.execute(f"INSERT INTO applications(username, preferredRole, motivation, userId) VALUES (?, ?, ?, ?)", (name, preferred_role, motivation, user_id)) return template("join_success") diff --git a/views/base.html b/views/base.html index cba0c5d..3f65912 100644 --- a/views/base.html +++ b/views/base.html @@ -15,7 +15,6 @@
  • About us
  • History
  • Join
  • -
  • Log in
  • {% block content %}{% endblock %}
    diff --git a/views/join_form.html b/views/join_form.html index 0e06399..440c993 100644 --- a/views/join_form.html +++ b/views/join_form.html @@ -16,6 +16,7 @@ {% block content %}