cleaned backend

This commit is contained in:
Alfie King 2025-06-22 15:56:38 +01:00
parent da447939bb
commit f3d5cb9d53
20 changed files with 5726 additions and 169 deletions

5333
app.log Normal file

File diff suppressed because it is too large Load Diff

BIN
db.sqlite

Binary file not shown.

View File

@ -19,7 +19,7 @@ COPY static static
EXPOSE 5000 EXPOSE 5000
# Set environment variables # Set environment variables
ENV FLASK_APP=main.py ENV FLASK_APP=app.py
# run the application # run the application
ENTRYPOINT [ "gunicorn", "-b", ":5000", "--access-logfile", "-", "--error-logfile", "-", "src.main:app" ] ENTRYPOINT [ "gunicorn", "-b", ":5000", "--access-logfile", "-", "--error-logfile", "-", "src.wsgi:app" ]

5
run.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
[ ! -f .env ] || export $(grep -v '^#' .env | xargs)
flask --app src.wsgi --debug run

View File

@ -1,42 +0,0 @@
import sqlite3
class Database:
def __init__(self, db_name='db.sqlite'):
self.connection = sqlite3.connect(db_name, check_same_thread=False)
self.cursor = self.connection.cursor()
self.create_snake_table()
def create_snake_table(self):
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS snake (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
score INTEGER NOT NULL
)
''')
self.connection.commit()
def insert_snake(self, name, score):
old_score = self.get_snake_score(name)
print(f"Old score for {name}: {old_score}")
print(f"New score for {name}: {score}")
if old_score is not None and score <= old_score:
return
self.cursor.execute('''
INSERT INTO snake (name, score)
VALUES (?, ?)
''', (name, score))
self.connection.commit()
def get_snake_score(self, name):
self.cursor.execute('SELECT score FROM snake WHERE name = ? ORDER BY score DESC LIMIT 1', (name,))
result = self.cursor.fetchone()
return result[0] if result else None
def get_snake_scores(self):
self.cursor.execute('SELECT * FROM snake ORDER BY score DESC')
return self.cursor.fetchall()
def close(self):
self.connection.close()

View File

@ -1,117 +0,0 @@
from flask import Flask, request, render_template, send_from_directory, abort
from flask_session import Session
from dotenv import load_dotenv
from os import getenv as env
import logging, requests
try:
import src.database as database
except ImportError:
import database
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
load_dotenv()
app = Flask(
__name__,
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
static_folder=env('STATIC_FOLDER', default='../static'),
static_url_path=env('STATIC_URL_PATH', default='/static')
)
app.config["SESSION_PERMANENT"] = True
app.config["SESSION_TYPE"] = "filesystem"
Session(app)
db = database.Database(db_name=env('DB_NAME', default='db.sqlite'))
@app.route('/')
def index():
return render_template('index.html')
@app.route('/robots.txt')
@app.route('/sitemap.xml')
@app.route('/favicon.ico')
def web_stuffs():
return send_from_directory(
app.static_folder,
request.path[1:],
)
@app.route('/<path:path>')
def catch_all(path):
try:
return render_template(path + '.html')
except Exception as e:
abort(404)
@app.route('/404')
@app.errorhandler(404)
def not_found():
unformatted_scores = db.get_snake_scores()
scores = [{'position': i + 1, 'name': score[1], 'score': score[2]} for i, score in enumerate(unformatted_scores)]
return render_template('404.html', scores=scores)
@app.route('/404/submit', methods=['POST'])
def snake_submit():
unformatted_scores = db.get_snake_scores()
scores = [{'position': i + 1, 'name': score[1], 'score': score[2]} for i, score in enumerate(unformatted_scores)]
data = request.form
username = data.get('username', '').strip()
score = data.get('snake-score', '').strip()
token = data.get('cap-token', '').strip()
if not username or not score or not token:
logging.error("Missing required fields: username=%s, score=%s, token=%s", username, score, token)
return render_template('404.html', scores=scores, error='Missing required fields'), 400
try:
score = int(score)
except ValueError:
logging.error("Invalid score value: %s", score)
return render_template('404.html', scores=scores, error='Invalid score value'), 400
if score <= 0 or score > 10000 or len(username) < 3 or len(username) > 15:
logging.error("Invalid score or username length: score=%s, username=%s", score, username)
return render_template('404.html', scores=scores, error='Invalid score or username length'), 400
cap_response = requests.post(
env('CAP_VERIFY_URL', default='https://<instance_url>/<key_id>/siteverify'),
json={
'secret': env('CAP_SECRET', default=''),
'response': token,
}
)
if cap_response.status_code != 200 or not cap_response.json().get('success', "false") != "true":
logging.error("Captcha verification failed: %s", cap_response.json())
return render_template('404.html', scores=scores, error='Captcha verification failed'), 400
db.insert_snake(name=username, score=int(score))
logging.info("Snake submitted: name=%s, score=%d", username, score)
unformatted_scores = db.get_snake_scores()
scores = [{'position': i + 1, 'name': score[1], 'score': score[2]} for i, score in enumerate(unformatted_scores)]
return render_template('404.html', scores=scores, success='Score submitted successfully!')
@app.route('/500')
@app.errorhandler(500)
def internal_error(error="An internal server error occurred."):
logging.error("Internal server error: %s", error)
return render_template('500.html'), 500
if __name__ == '__main__':
app.run(
host=env('HOST', default='0.0.0.0'),
port=env('PORT', default=5000),
debug=env('DEBUG', default="false").lower() == 'true'
)

View File

@ -0,0 +1,48 @@
# Imports
from flask import Blueprint, render_template
from os import getenv as env
import logging
import src.routes.snake as snake
# Create blueprint
bp = Blueprint(
'error_handlers',
__name__,
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
static_folder=env('STATIC_FOLDER', default='../static')
)
# Create logger
log = logging.getLogger(__name__)
# Route for 500 error
@bp.route('/500')
@bp.app_errorhandler(500)
def internal_server_error(error=None):
if error is not None:
log.error("Internal server error: %s", error)
return render_template('errors/500.html'), 500
# Route for 404 error
@bp.route('/404')
@bp.app_errorhandler(404)
def not_found(error=None):
if error is not None:
log.warning("Page not found: %s", error)
scores = snake.get_leaderboard()
token = snake.generate_start_token()
return render_template('errors/404.html', scores=scores, token=token), 404 if error is not None else 200
# Route for 400 error
@bp.route('/400')
@bp.app_errorhandler(400)
def bad_request(error=None):
if error is not None:
log.warning("Bad request: %s", error)
return render_template('errors/400.html', error=error), 400

44
src/routes/generic.py Normal file
View File

@ -0,0 +1,44 @@
# Imports
from flask import Blueprint, render_template, request, abort, send_from_directory
from os import getenv as env
import logging
# Create blueprint
bp = Blueprint(
'generic',
__name__,
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
static_folder=env('STATIC_FOLDER', default='../static')
)
# Create logger
log = logging.getLogger(__name__)
# Route for index page
@bp.route('/')
def index():
return render_template('index.html')
# Route for robots.txt, sitemap.xml, and favicon.ico
@bp.route('/robots.txt')
@bp.route('/sitemap.xml')
@bp.route('/favicon.ico')
def web_stuffs():
return send_from_directory(
env('STATIC_FOLDER', default='../static'),
request.path.lstrip('/')
)
# catch-all route for any other static pages (only in root path)
@bp.route('/<string:filename>')
def static_files(filename):
try:
return render_template(filename if filename.endswith('.html') else filename + '.html')
except Exception as e:
log.error(f"Error serving static file {filename}: {e}")
abort(404)

127
src/routes/snake.py Normal file
View File

@ -0,0 +1,127 @@
# Imports
from flask import Blueprint, abort, request, redirect
from os import urandom, getenv as env
import src.utils.database as database
import src.utils.cap as cap
import logging, datetime, threading, time
# Create blueprint
bp = Blueprint(
'snake',
__name__,
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
static_folder=env('STATIC_FOLDER', default='../static')
)
# Create logger
log = logging.getLogger(__name__)
# Create database instance
db = database.Database(db_name=env('DB_NAME', default='db.sqlite'))
db.execute('CREATE TABLE IF NOT EXISTS snake_scores (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, score INTEGER)')
db.execute('CREATE TABLE IF NOT EXISTS snake_tokens (id INTEGER PRIMARY KEY AUTOINCREMENT, token TEXT UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)')
# Input validation function
def valid_length(value, min_length=1, max_length=100):
if not isinstance(value, str):
return False
return min_length <= len(value) <= max_length
def valid_score(score, game_token):
start_time = db.execute('SELECT created_at FROM snake_tokens WHERE token = ?', (game_token,)).fetchone()
if not start_time:
log.error("Game token not found.")
return False
start_time = datetime.datetime.fromisoformat(start_time[0])
current_time = datetime.datetime.now()
elapsed_time = (current_time - start_time).total_seconds()
if elapsed_time < score / 10 * 5 + 15: # assuming that each point takes 3 seconds to achieve and 15 seconds to start the game and do captcha
log.error("Score is too high for the elapsed time.")
return False
if score <= 0 or score > 10000: # Arbitrary upper limit for scores
log.error("Score is out of valid range.")
return False
return True
# Route for score submission
@bp.route('/snake/submit', methods=['POST'])
def submit_score():
name = request.form.get('name')
score = request.form.get('score')
captcha_token = request.form.get('cap-token')
game_token = request.form.get('game_token')
if not cap.verify_captcha(captcha_token):
log.error("Captcha verification failed.")
abort(400, "Captcha verification failed")
if not name or not score or not captcha_token or not game_token:
log.error("Name, score, captcha token, or game token is missing.")
abort(400, "Missing required fields")
if not valid_length(name, min_length=3, max_length=15):
log.error("Invalid name length.")
abort(400, "Name must be between 3 and 15 characters long.")
if not valid_score(int(score), game_token):
log.error("Invalid score.")
abort(400, "Score not vilid, so either you are trying to cheat the leaderboard or something is seriously wrong.")
try:
db.execute('INSERT INTO snake_scores (name, score) VALUES (?, ?)', (name, int(score)))
db.execute('DELETE FROM snake_tokens WHERE token = ?', (game_token,))
log.info(f"Score submitted: {name} - {score}")
return redirect('/404')
except Exception as e:
log.error(f"Database error: {e}")
abort(500, "Internal server error while submitting score.")
# Generate a unique game token
def generate_start_token():
"""Generate a unique start token for the game."""
token = urandom(16).hex()
log.info(f"Generated start token: {token}")
db.execute('INSERT INTO snake_tokens (token) VALUES (?)', (token,))
return token
# Get leaderboard scores
def get_leaderboard():
"""Fetch scores from the leaderboard."""
try:
scores = db.execute('SELECT name, score FROM snake_scores ORDER BY score DESC').fetchall()
leaderboard = [{'position': i + 1, 'name': score[0], 'score': score[1]} for i, score in enumerate(scores)]
log.info("Leaderboard fetched successfully.")
return leaderboard
except Exception as e:
log.error(f"Error fetching leaderboard: {e}")
return []
# Clear all tokens older than 1 hour
def clear_old_tokens():
while True:
try:
one_hour_ago = datetime.datetime.now() - datetime.timedelta(hours=1)
db.execute('DELETE FROM snake_tokens WHERE created_at < ?', (one_hour_ago,))
log.info("Old tokens cleared.")
except Exception as e:
log.error(f"Error clearing old tokens: {e}")
time.sleep(3600) # Run every hour
# Start the token clearing thread
token_thread = threading.Thread(target=clear_old_tokens, daemon=True)
token_thread.start()

42
src/utils/cap.py Normal file
View File

@ -0,0 +1,42 @@
# Imports
from os import getenv as env
import requests, logging
# Create logger
log = logging.getLogger(__name__)
# Function to verify CAPTCHA response
def verify_captcha(token: str) -> bool:
"""
Verify the CAP response token with the CAP server.
Args:
token (str): The CAP response token to verify.
Returns:
bool: True if the token is valid, False otherwise.
"""
if not token:
return False
try:
response = requests.post(
env('CAP_VERIFY_URL', default='https://<instance_url>/<key_id>/siteverify'),
json={
'secret': env('CAP_SECRET', default=''),
'response': token,
},
timeout=10
)
response.raise_for_status()
if response.status_code != 200:
log.error("CAPTCHA verification failed with status code: %s", response.status_code)
return False
return response.json().get('success', False)
except Exception as e:
log.error("Error verifying CAPTCHA: %s", e)
return False

26
src/utils/database.py Normal file
View File

@ -0,0 +1,26 @@
# Imports
import sqlite3
# Database class
class Database:
def __init__(self, db_name='db.sqlite'):
self.connection = sqlite3.connect(db_name, check_same_thread=False)
self.cursor = self.connection.cursor()
def execute(self, query, params=None):
if params is None:
params = []
self.cursor.execute(query, params)
self.connection.commit()
return self.cursor
def fetchall(self, query, params=None):
cursor = self.execute(query, params)
return cursor.fetchall()
def fetchone(self, query, params=None):
cursor = self.execute(query, params)
return cursor.fetchone()
def close(self):
self.connection.close()

58
src/wsgi.py Normal file
View File

@ -0,0 +1,58 @@
# Imports
from flask import Flask, request, render_template, send_from_directory, abort
from flask_session import Session
from dotenv import load_dotenv
from os import getenv as env, listdir
import logging, importlib
# Load env
load_dotenv()
# Create console log handler
console_log = logging.StreamHandler()
console_log.setFormatter(logging.Formatter("\033[1;32m%(asctime)s\033[0m - \033[1;34m%(levelname)s\033[0m - \033[1;31m%(name)s\033[0m - %(message)s"))
console_log.setLevel(logging.INFO)
# Create file log handler
file_log = logging.FileHandler(env('LOG_FILE', default='app.log'), mode=env('LOG_MODE', default='a'))
file_log.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s"))
file_log.setLevel(logging.DEBUG)
# Add handlers to the logger
log = logging.getLogger()
log.setLevel(logging.DEBUG)
log.addHandler(console_log)
log.addHandler(file_log)
log.info("Logging initialized")
# Create flask app
app = Flask(
__name__,
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
static_folder=env('STATIC_FOLDER', default='../static')
)
# Configure sessions
app.config["SESSION_PERMANENT"] = True
app.config["SESSION_TYPE"] = "filesystem"
Session(app)
# Load routes
routes_dir = env('ROUTES_DIR', default='src/routes')
for filename in listdir(routes_dir):
if not filename.endswith('.py') and filename.startswith('__'):
continue
module_name = f"{routes_dir.replace('/', '.')}.{filename[:-3]}"
try:
module = importlib.import_module(module_name)
if hasattr(module, 'bp'):
app.register_blueprint(module.bp)
log.info(f"Registered blueprint: {module_name}")
except Exception as e:
log.error(f"Failed to register blueprint {module_name}: {e}")

3
static/css/400.css Normal file
View File

@ -0,0 +1,3 @@
section h2 {
margin-top: 2rem !important;
}

View File

@ -154,7 +154,7 @@ function gameLoop() {
setTimeout(gameLoop, 100); setTimeout(gameLoop, 100);
} else { } else {
document.removeEventListener('keydown', changeDirection); document.removeEventListener('keydown', changeDirection);
document.getElementById('snake-score').value = score; document.getElementById('score').value = score;
alert(`Game Over! Your score: ${score}`); alert(`Game Over! Your score: ${score}`);
menu(); menu();
} }

29
templates/errors/400.html Normal file
View File

@ -0,0 +1,29 @@
{% extends "bases/base.html" %}
{% block title %}400 - Internal Server Error{% endblock %}
{% block description %}Bad request. The server could not understand the request due to invalid syntax.{% endblock %}
{% block head %}
<link rel="stylesheet" href="/static/css/400.css">
{% endblock %}
{% block content %}
<section>
<h1>400 - Bad Request</h1>
<p>
What did you do? The server could not understand the request due to invalid syntax. Please check your request and try again.
</p>
<h2>The fuckup in question</h2>
<p>
{% if error %}
{{ error }}
{% else %}
No specific error message provided.
{% endif %}
</p>
<h2>What to do now</h2>
<p>
idk :P
</p>
</section>
{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "bases/base.html" %}
{% block title %}404 - Not Found{% endblock %} {% block title %}404 - Not Found{% endblock %}
{% block description %}The page you are looking for does not exist.{% endblock %} {% block description %}The page you are looking for does not exist.{% endblock %}
@ -28,10 +28,11 @@
<section class="pcOnly flex-row"> <section class="pcOnly flex-row">
<section class="min-width"> <section class="min-width">
<h2>Submit score</h2> <h2>Submit score</h2>
<form action="/404/submit" method="POST" id="snakeForm"> <form action="/snake/submit" method="POST" id="snakeForm">
<input type="text" id="username" name="username" placeholder="Your name" required> <input type="text" id="name" name="name" placeholder="Your name" required>
<cap-widget id="captcha" data-cap-api-endpoint="https://cap.alfieking.dev/57d36430b9cb/api/"></cap-widget> <cap-widget id="captcha" data-cap-api-endpoint="https://cap.alfieking.dev/57d36430b9cb/api/"></cap-widget>
<input type="hidden" id="snake-score" name="snake-score" value="0"> <input type="hidden" id="score" name="score" value="0">
<input type="hidden" id="game_token" name="game_token" value="{{ token}}">
<button type="submit" id="submit">Submit</button> <button type="submit" id="submit">Submit</button>
</form> </form>
</section> </section>

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "bases/base.html" %}
{% block title %}500 - Internal Server Error{% endblock %} {% block title %}500 - Internal Server Error{% endblock %}
{% block description %}An unexpected error occurred on the server.{% endblock %} {% block description %}An unexpected error occurred on the server.{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "bases/base.html" %}
{% block title %}Home - Alfie's basement{% endblock %} {% block title %}Home - Alfie's basement{% endblock %}
{% block description %}server backend survivor{% endblock %} {% block description %}server backend survivor{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "base.html" %} {% extends "bases/base.html" %}
{% block title %}Toaster - Alfie's basement{% endblock %} {% block title %}Toaster - Alfie's basement{% endblock %}
{% block description %}furry corner{% endblock %} {% block description %}furry corner{% endblock %}