diff --git a/TFR/Dockerfile b/TFR/Dockerfile index 840cc19..0f25da0 100644 --- a/TFR/Dockerfile +++ b/TFR/Dockerfile @@ -5,7 +5,7 @@ EXPOSE 8000 WORKDIR /data RUN mkdir /storage&& \ apk update && \ - apk add python3 py3-pip postgresql-client + apk --no-cache add python3 py3-pip postgresql-client COPY requirements.txt requirements.txt RUN pip install --no-cache-dir -r requirements.txt 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..457df68 100644 --- a/TFR/server/account.py +++ b/TFR/server/account.py @@ -21,160 +21,167 @@ 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")) \ No newline at end of file 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/search.js b/TFR/server/static/js/search.js index 70d782b..c2fd1c0 100644 --- a/TFR/server/static/js/search.js +++ b/TFR/server/static/js/search.js @@ -1,7 +1,7 @@ function showHint() { const search = document.querySelector('#search'); const searchPos = search.getBoundingClientRect(); - let hint = document.querySelector('.search-hint'); + const hint = document.querySelector('.search-hint'); hint.style.width = `${search.offsetWidth}px`; hint.style.left = `${searchPos.left}px`; @@ -12,7 +12,7 @@ function showHint() { function hideHint() { - let hint = document.querySelector('.search-hint'); + const hint = document.querySelector('.search-hint'); hint.style.display = 'none'; } @@ -20,7 +20,7 @@ function hideHint() { function updateHint() { const search = document.querySelector('#search'); const searchPos = search.getBoundingClientRect(); - let hint = document.querySelector('.search-hint'); + const hint = document.querySelector('.search-hint'); hint.style.width = `${search.offsetWidth}px`; hint.style.left = `${searchPos.left}px`; @@ -30,7 +30,7 @@ function updateHint() { 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...

'; @@ -68,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/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")