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 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app.py') 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") -- 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 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'app.py') 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}/") -- 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(-) (limited to 'app.py') 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(-) (limited to 'app.py') 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 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) (limited to 'app.py') 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") -- cgit v1.2.3 From 692a7fc3a5b4e6c655732c1b29274b66bea515d9 Mon Sep 17 00:00:00 2001 From: Linnnus Date: Mon, 29 Apr 2024 10:31:06 +0200 Subject: Ensure unique applicants --- app.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'app.py') diff --git a/app.py b/app.py index 73926e3..cc16026 100644 --- a/app.py +++ b/app.py @@ -27,7 +27,7 @@ cursor.executescript(""" username VARCHAR(12) NOT NULL, preferredRole VARCHAR(6) NOT NULL, motivation TEXT NOT NULL, - userId INTEGER NOT NULL + userId INTEGER UNIQUE NOT NULL ); """) cursor.close() @@ -92,8 +92,16 @@ def join_submission(db: sqlite3.Connection): if user_id == None or not user_id.isdigit(): raise HTTPError(400, "Missing or invalid user id") - # 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)) + try: + db.execute("INSERT INTO applications(username, preferredRole, motivation, userId) VALUES (?, ?, ?, ?)", (name, preferred_role, motivation, user_id)) + except sqlite3.IntegrityError as e: + print(e.sqlite_errorcode == sqlite3.SQLITE_CONSTRAINT_UNIQUE) + print(str(e)) + if e.sqlite_errorcode == sqlite3.SQLITE_CONSTRAINT_UNIQUE: + # The database (model) rejected the application because the unique constraint wasn't met! + raise HTTPError(400, "You've already submitted an application!") + else: + raise return template("join_success") -- cgit v1.2.3