diff --git a/TFR/Dockerfile b/TFR/Dockerfile index 130c6aa..0f25da0 100644 --- a/TFR/Dockerfile +++ b/TFR/Dockerfile @@ -1,14 +1,15 @@ # syntax=docker/dockerfile:1 -FROM alpine:latest +FROM alpine:3.18.2 EXPOSE 8000 -RUN apk update && apk add python3 py3-pip postgresql-client WORKDIR /data +RUN mkdir /storage&& \ + apk update && \ + apk --no-cache add python3 py3-pip postgresql-client COPY requirements.txt requirements.txt -RUN pip install -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt -RUN mkdir /storage COPY ./server ./server COPY ./run.sh ./run.sh RUN chmod +x ./run.sh diff --git a/TFR/run.sh b/TFR/run.sh index a75e82e..353db65 100644 --- a/TFR/run.sh +++ b/TFR/run.sh @@ -1,15 +1,15 @@ #!/bin/sh # Wait for database to start -until pg_isready -d $DB_NAME -h $DB_HOST -U $DB_USER +until pg_isready -d "$DB_NAME" -h "$DB_HOST" -U "$DB_USER" do - echo "Waiting for database to start... (5s)" - sleep 5 + echo "Waiting for database to start... (3s)" + sleep 3 done echo "Database is ready!" -# Check if migrastions folder exists +# Check if migrations folder exists if [ ! -d "/data/storage/migrations" ]; then echo "Creating tables..." @@ -17,7 +17,7 @@ then fi # Check if there are any changes to the database -if ! $(flask --app server db check | grep -q "No changes in schema detected."); +if ! flask --app server db check | grep "No changes in schema detected."; then echo "Database changes detected! Migrating..." flask --app server db migrate diff --git a/TFR/server/account.py b/TFR/server/account.py index f94bdfb..badb41b 100644 --- a/TFR/server/account.py +++ b/TFR/server/account.py @@ -21,160 +21,165 @@ from .extensions import db blueprint = Blueprint("account", __name__, url_prefix="/account") -@blueprint.route("/settings", methods=["GET", "POST"]) +@blueprint.route("/settings", methods=["GET"]) @login_required -def settings(): - if request.method == "POST": - username = request.form.get("username", "").strip() - email = request.form.get("email", "").strip() - password = request.form.get("password", "").strip() - error = [] +def get_settings(): + action = request.args.get("action", None) - user = Users.query.filter_by(username=current_user.username).first() + if action == "logout": + logout_user() + flash("Successfully logged out!", "success") + return redirect(url_for("views.index")) - if not check_password_hash(user.password, password): - flash("Password is incorrect!", "error") - return redirect(url_for("account.settings")) + sessions = Sessions.query.filter_by(user_id=current_user.id).all() + return render_template("views/account_settings.html", sessions=sessions) - if "file" in request.files and request.files["file"].filename: - picture = request.files["file"] - file_ext = picture.filename.split(".")[-1].lower() - file_name = f"{user.id}.{file_ext}" - if file_ext not in UPLOAD_EXTENSIONS: - error.append("Picture is not a valid image!") - if picture.content_length > UPLOAD_MAX_SIZE: - error.append( - f"Picture must be less than {UPLOAD_EXTENSIONS / 1000000}MB!" - ) +@blueprint.route("/settings", methods=["POST"]) +@login_required +def post_settings(): + username = request.form.get("username", "").strip() + email = request.form.get("email", "").strip() + password = request.form.get("password", "").strip() + error = [] - image = Image.open(picture.stream) + user = Users.query.filter_by(username=current_user.username).first() - # Resizing gifs is more work than it's worth - if file_ext != "gif": - image_x, image_y = image.size - image.thumbnail( - (min(image_x, UPLOAD_RESOLUTION), min(image_y, UPLOAD_RESOLUTION)) - ) - - if error: - for err in error: - flash(err, "error") - return redirect(url_for("account.settings")) - - if user.picture: - os.remove(os.path.join(UPLOAD_DIR, user.picture)) - - user.picture = file_name - - if file_ext == "gif": - image.save(os.path.join(UPLOAD_DIR, file_name), save_all=True) - else: - image.save(os.path.join(UPLOAD_DIR, file_name)) - - image.close() - - if username: - if USER_REGEX.match(username): - user.username = username - else: - error.append("Username is invalid!") - if email: - if USER_EMAIL_REGEX.match(email): - user.email = email - else: - error.append("Email is invalid!") - - if error: - for err in error: - flash(err, "error") - return redirect(url_for("account.settings")) - - db.session.commit() - - flash("Successfully updated account!", "success") + if not check_password_hash(user.password, password): + flash("Password is incorrect!", "error") return redirect(url_for("account.settings")) - else: - action = request.args.get("action", None) - if action == "logout": - logout_user() - flash("Successfully logged out!", "success") - return redirect(url_for("views.index")) + if "file" in request.files and request.files["file"].filename: + picture = request.files["file"] + file_ext = picture.filename.split(".")[-1].lower() + file_name = f"{user.id}.{file_ext}" - sessions = Sessions.query.filter_by(user_id=current_user.id).all() - return render_template("views/account_settings.html", sessions=sessions) + if file_ext not in UPLOAD_EXTENSIONS: + error.append("Picture is not a valid image!") + if picture.content_length > UPLOAD_MAX_SIZE: + error.append(f"Picture must be less than {UPLOAD_MAX_SIZE}MB!") + image = Image.open(picture.stream) -@blueprint.route("/reset-password", methods=["GET", "POST"]) -@login_required -def password_reset(): - if request.method == "POST": - current = request.form.get("current", "").strip() - new = request.form.get("new", "").strip() - confirm = request.form.get("confirm", "").strip() - error = [] - - user = Users.query.filter_by(username=current_user.username).first() - - if not current or not new or not confirm: - error.append("Please fill out all fields!") - if not check_password_hash(user.password, current): - error.append("Current password is incorrect!") - if len(new) < 8: - error.append( - "New password is too short! Must be at least 8 characters long." + # Resizing gifs is more work than it's worth + if file_ext != "gif": + image_x, image_y = image.size + image.thumbnail( + (min(image_x, UPLOAD_RESOLUTION), min(image_y, UPLOAD_RESOLUTION)) ) - if new != confirm: - error.append("New passwords do not match!") if error: for err in error: flash(err, "error") - return redirect(url_for("account.password_reset")) + return redirect(url_for("account.settings")) - user.password = generate_password_hash(new, method="scrypt") - user.alt_id = str(uuid.uuid4()) - db.session.commit() + if user.picture: + os.remove(os.path.join(UPLOAD_DIR, user.picture)) - flash("Successfully changed password!", "success") - logout_user() - return redirect(url_for("auth.auth")) - else: - return render_template("views/reset_password.html") + user.picture = file_name + + if file_ext == "gif": + image.save(os.path.join(UPLOAD_DIR, file_name), save_all=True) + else: + image.save(os.path.join(UPLOAD_DIR, file_name)) + + image.close() + + if username: + if USER_REGEX.match(username): + user.username = username + else: + error.append("Username is invalid!") + if email: + if USER_EMAIL_REGEX.match(email): + user.email = email + else: + error.append("Email is invalid!") + + if error: + for err in error: + flash(err, "error") + return redirect(url_for("account.settings")) + + db.session.commit() + + flash("Successfully updated account!", "success") + return redirect(url_for("account.settings")) -@blueprint.route("/delete-account", methods=["GET", "POST"]) +@blueprint.route("/password", methods=["GET"]) @login_required -def delete_account(): - if request.method == "POST": - username = request.form.get("username", "").strip() - password = request.form.get("password", "").strip() - error = [] +def get_password_reset(): + return render_template("views/reset_password.html") - user = Users.query.filter_by(username=current_user.username).first() - if username != user.username: - error.append("Username does not match!") - if not password: - error.append("Please fill out all fields!") - if not check_password_hash(user.password, password): - error.append("Password is incorrect!") +@blueprint.route("/password", methods=["POST"]) +@login_required +def post_password_reset(): + current = request.form.get("current", "").strip() + new = request.form.get("new", "").strip() + confirm = request.form.get("confirm", "").strip() + error = [] - if error: - for err in error: - flash(err, "error") - return redirect(url_for("account.delete_account")) + user = Users.query.filter_by(username=current_user.username).first() - db.session.query(Sessions).filter_by(user_id=current_user.id).delete() - db.session.query(Scores).filter_by(user_id=current_user.id).delete() - db.session.query(ProfileTags).filter_by(user_id=current_user.id).delete() - db.session.query(PasswordReset).filter_by(user_id=current_user.id).delete() - db.session.delete(user) - db.session.commit() + if not current or not new or not confirm: + error.append("Please fill out all fields!") + if not check_password_hash(user.password, current): + error.append("Current password is incorrect!") + if len(new) < 8: + error.append("New password is too short! Must be at least 8 characters long.") + if new != confirm: + error.append("New passwords do not match!") - flash("Successfully deleted account!", "success") - logout_user() - return redirect(url_for("auth.auth")) - else: - return render_template("views/delete_account.html") + if error: + for err in error: + flash(err, "error") + return redirect(url_for("account.password_reset")) + + user.password = generate_password_hash(new, method="scrypt") + user.alt_id = str(uuid.uuid4()) + db.session.commit() + + flash("Successfully changed password!", "success") + logout_user() + return redirect(url_for("auth.auth")) + + +@blueprint.route("/delete-account", methods=["GET"]) +@login_required +def get_delete_account(): + return render_template("views/delete_account.html") + + +@blueprint.route("/delete", methods=["POST"]) +@login_required +def post_delete_account(): + username = request.form.get("username", "").strip() + password = request.form.get("password", "").strip() + error = [] + + user = Users.query.filter_by(username=current_user.username).first() + + if username != user.username: + error.append("Username does not match!") + if not password: + error.append("Please fill out all fields!") + if not check_password_hash(user.password, password): + error.append("Password is incorrect!") + + if error: + for err in error: + flash(err, "error") + return redirect(url_for("account.delete_account")) + + db.session.query(Sessions).filter_by(user_id=current_user.id).delete() + db.session.query(Scores).filter_by(user_id=current_user.id).delete() + db.session.query(ProfileTags).filter_by(user_id=current_user.id).delete() + db.session.query(PasswordReset).filter_by(user_id=current_user.id).delete() + db.session.delete(user) + db.session.commit() + + flash("Successfully deleted account!", "success") + logout_user() + return redirect(url_for("auth.auth")) diff --git a/TFR/server/config.py b/TFR/server/config.py index 522dae0..9700d34 100644 --- a/TFR/server/config.py +++ b/TFR/server/config.py @@ -6,7 +6,7 @@ SECRET_KEY = getenv("FLASK_KEY") UPLOAD_DIR = "/data/uploads" UPLOAD_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp"] -UPLOAD_RESOLUTION = 512 +UPLOAD_RESOLUTION = 169 UPLOAD_MAX_SIZE = 3 * 1024 * 1024 # 3MB GAME_VERSION = "alpha" diff --git a/TFR/server/static/js/flash.js b/TFR/server/static/js/flash.js index b9807a6..3c2f0bf 100644 --- a/TFR/server/static/js/flash.js +++ b/TFR/server/static/js/flash.js @@ -1,10 +1,10 @@ function addFlashMessage(message, type='success') { - let flask = document.createElement('p'); + const flask = document.createElement('p'); flask.onclick = () => flask.remove(); flask.classList.add(type); flask.innerHTML = message; - let close = document.createElement('span'); + const close = document.createElement('span'); close.innerHTML = ''; flask.appendChild(close); diff --git a/TFR/server/static/js/search.js b/TFR/server/static/js/search.js index da4cc23..4cc1638 100644 --- a/TFR/server/static/js/search.js +++ b/TFR/server/static/js/search.js @@ -1,43 +1,43 @@ function showHint() { - let search = document.querySelector('#search'); - let searchPos = search.getBoundingClientRect(); - let hint = document.querySelector('.search-hint'); + const search = document.querySelector('#search'); + const searchPos = search.getBoundingClientRect(); + const hint = document.querySelector('.search-hint'); - hint.style.width = search.offsetWidth + 'px'; - hint.style.left = searchPos.left + 'px'; - hint.style.top = searchPos.bottom + 'px'; + hint.style.width = `${search.offsetWidth}px`; + hint.style.left = `${searchPos.left}px`; + hint.style.top = `${searchPos.bottom}px`; hint.style.display = 'flex'; } function hideHint() { - let hint = document.querySelector('.search-hint'); + const hint = document.querySelector('.search-hint'); hint.style.display = 'none'; } function updateHint() { - let search = document.querySelector('#search'); - let searchPos = search.getBoundingClientRect(); - let hint = document.querySelector('.search-hint'); + const search = document.querySelector('#search'); + const searchPos = search.getBoundingClientRect(); + const hint = document.querySelector('.search-hint'); - hint.style.width = search.offsetWidth + 'px'; - hint.style.left = searchPos.left + 'px'; - hint.style.top = searchPos.bottom + 'px'; + hint.style.width = `${search.offsetWidth}px`; + hint.style.left = `${searchPos.left}px`; + hint.style.top = `${searchPos.bottom}px`; } function getSearch() { let search = document.querySelector('#search').value; - let hint = document.querySelector('.search-hint'); + const hint = document.querySelector('.search-hint'); if (search.length === 0) { hint.innerHTML = '

Start typing to see results...

'; return; } - fetch('/api/search?q=' + search.toString(), { + fetch(`/api/search?q=${search}`, { method: 'GET', }) .then(response => response.json()) @@ -50,12 +50,11 @@ function getSearch() { hint.innerHTML = ''; data.forEach(user => { - let el = document.createElement('button'); + const el = document.createElement('button'); el.innerHTML = user; el.onmousedown = function (event) { event.preventDefault(); - search = document.querySelector('#search'); - search.value = user.toString(); + search = user.toString(); search.blur(); } hint.appendChild(el); @@ -69,7 +68,7 @@ function getSearch() { window.onload = () => { let typingTimer; - let search = document.querySelector('#search'); + const search = document.querySelector('#search'); if (search === null) { return; diff --git a/TFR/server/static/js/sessions.js b/TFR/server/static/js/sessions.js index e2ec13a..becb7c7 100644 --- a/TFR/server/static/js/sessions.js +++ b/TFR/server/static/js/sessions.js @@ -1,6 +1,6 @@ function yeetSession(id) { - let form = new FormData(); - form.append('session_id', id); + const form = new FormData(); + form.append('session', id); fetch('/api/tokens', { method: 'POST', diff --git a/TFR/server/templates/navigation.html b/TFR/server/templates/navigation.html index e349325..ab14234 100644 --- a/TFR/server/templates/navigation.html +++ b/TFR/server/templates/navigation.html @@ -5,7 +5,7 @@ {% if current_user.is_authenticated %} - + {{ current_user.username }} {% if not current_user.email %}{% endif %} diff --git a/TFR/server/templates/views/account_settings.html b/TFR/server/templates/views/account_settings.html index 5a450fb..e92eda7 100644 --- a/TFR/server/templates/views/account_settings.html +++ b/TFR/server/templates/views/account_settings.html @@ -4,7 +4,7 @@ {% block content %}

Profile Settings

-
+
{% if current_user.picture %} @@ -34,7 +34,7 @@
- + @@ -44,7 +44,7 @@ - + {% endfor %}
Options Device Created Last Used {{ session.device_type }} {{ session.created_at.strftime('%Y-%m-%d') }}{{ session.last_used.strftime('%Y-%m-%d') }}{{ session.last_used|timesince }}
@@ -54,9 +54,9 @@ @@ -67,8 +67,7 @@ // Adjusted from https://stackoverflow.com/a/3814285/14885829 document.getElementById('profile-picture').onchange = (event) => { - let tgt = event.target || window.event.srcElement, - files = tgt.files; + let files = event.target.files; if (FileReader && files && files.length) { let fr = new FileReader(); @@ -78,7 +77,7 @@ fr.readAsDataURL(files[0]); } else { - addFlashMessage("Your browser could not show a preview of your profile picture!", "error") + addFlashMessage("Your browser could not show a preview of your profile picture!", "error"); } } diff --git a/TFR/server/templates/views/delete_account.html b/TFR/server/templates/views/delete_account.html index bb4f42e..6b3fa86 100644 --- a/TFR/server/templates/views/delete_account.html +++ b/TFR/server/templates/views/delete_account.html @@ -8,7 +8,7 @@ Deleting your account will delete EVERYTHING on your account, including ALL your ever submitted scores. There is NO WAY to recover your account from this, are you sure you want todo this?

- + {{ text(id="username", name="username", required=True) }} {{ text(id="password", name="password", type="password", required=True) }} diff --git a/TFR/server/templates/views/reset_password.html b/TFR/server/templates/views/reset_password.html index 85e21e8..59e2641 100644 --- a/TFR/server/templates/views/reset_password.html +++ b/TFR/server/templates/views/reset_password.html @@ -5,7 +5,7 @@

Password Reset

Forgotten your current password? Go here [insert password reset tool link]

- + {{ text(id="current-password", name="current", type="password", required=True) }} {{ text(id="new-password", name="new", type="password", required=True) }} {{ text(id="confirm-password", name="confirm", type="password", required=True) }} diff --git a/TFR/server/views.py b/TFR/server/views.py index e6bbc36..5dfdf78 100644 --- a/TFR/server/views.py +++ b/TFR/server/views.py @@ -9,7 +9,7 @@ from .extensions import db blueprint = Blueprint("views", __name__) -@blueprint.route("/") +@blueprint.route("/", methods=["GET"]) def index(): diff_arg = request.args.get("diff", 0) ver_arg = request.args.get("ver", GAME_VERSION).strip() @@ -45,6 +45,6 @@ def index(): ) -@blueprint.route("/about") +@blueprint.route("/about", methods=["GET"]) def about(): return render_template("views/about.html")