diff --git a/TFR/server/__init__.py b/TFR/server/__init__.py index e910a85..053fc46 100644 --- a/TFR/server/__init__.py +++ b/TFR/server/__init__.py @@ -6,7 +6,7 @@ from werkzeug.exceptions import HTTPException from .extensions import db, migrate, cache, assets, login_manager from .models import Users -from . import views, auth, api, filters +from . import views, account, auth, api, filters app = Flask(__name__) @@ -36,6 +36,7 @@ assets.register("styles", styles) cache.init_app(app) app.register_blueprint(views.blueprint) +app.register_blueprint(account.blueprint) app.register_blueprint(auth.blueprint) app.register_blueprint(api.blueprint) app.register_blueprint(filters.blueprint) diff --git a/TFR/server/account.py b/TFR/server/account.py new file mode 100644 index 0000000..519f378 --- /dev/null +++ b/TFR/server/account.py @@ -0,0 +1,135 @@ +import uuid +import re + +from flask import Blueprint, request, render_template, flash, redirect, url_for +from flask_login import login_required, current_user, logout_user +from werkzeug.security import generate_password_hash, check_password_hash + +from .config import USER_REGEX, USER_EMAIL_REGEX +from .models import Users, Sessions, Scores, ProfileTags, PasswordReset +from .extensions import db + + +blueprint = Blueprint("account", __name__, url_prefix="/account") + + +@blueprint.route("/settings", methods=["GET", "POST"]) +@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() + + user_regex = re.compile(USER_REGEX) + email_regex = re.compile(USER_EMAIL_REGEX) + error = [] + + user = Users.query.filter_by(username=current_user.username).first() + + if not check_password_hash(user.password, password): + flash("Password is incorrect!", "error") + return redirect(url_for('account.settings')) + + if username: + if user_regex.match(username): + user.username = username + else: + error.append("Username is invalid!") + if email: + if 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")) + else: + action = request.args.get("action", None) + + if action == "logout": + logout_user() + flash("Successfully logged out!", "success") + return redirect(url_for("views.index")) + + sessions = Sessions.query.filter_by(user_id=current_user.id).all() + return render_template("views/account_settings.html", sessions=sessions) + + +@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.") + 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")) + + 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")) + else: + return render_template("views/reset_password.html") + + +@blueprint.route("/delete-account", methods=["GET", "POST"]) +@login_required +def delete_account(): + if request.method == "POST": + 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")) + else: + return render_template("views/delete_account.html") diff --git a/TFR/server/api.py b/TFR/server/api.py index 1513d4a..47b91e3 100644 --- a/TFR/server/api.py +++ b/TFR/server/api.py @@ -20,7 +20,7 @@ blueprint = Blueprint("api", __name__, url_prefix="/api") @blueprint.route("/tokens", methods=["POST"]) @login_required def tokens(): - session_id = request.form["session_id"] + session_id = request.form.get("session", "").strip() if not session_id: return jsonify({"error": "No Session provided!"}), 400 @@ -40,8 +40,8 @@ def tokens(): @blueprint.route("/post", methods=["POST"]) def post(): - session_key = request.form.get("session", None) - version = request.form.get("version", "alpha") + session_key = request.form.get("session", "").strip() + version = request.form.get("version", "alpha").strip() difficulty = request.form.get("difficulty", 0) score = request.form.get("score", 0) @@ -94,8 +94,8 @@ def search(): @blueprint.route("/login", methods=["POST"]) def login(): - username = request.form.get("username", None).strip() - password = request.form.get("password", None).strip() + username = request.form.get("username", "").strip() + password = request.form.get("password", "").strip() device = request.form.get("device", "Unknown").strip() username_regex = re.compile(USER_REGEX) @@ -120,7 +120,7 @@ def login(): @blueprint.route("/authenticate", methods=["POST"]) def authenticate(): - auth_key = request.form.get("session", None).strip() + auth_key = request.form.get("session", "").strip() session = Sessions.query.filter_by(auth_key=auth_key).first() if not session: diff --git a/TFR/server/auth.py b/TFR/server/auth.py index 272e5ff..762aab6 100644 --- a/TFR/server/auth.py +++ b/TFR/server/auth.py @@ -81,4 +81,4 @@ def login(): login_user(user, remember=True) flash("Successfully logged in!", "success") - return redirect(url_for("views.index")) + return redirect(url_for("account.settings")) diff --git a/TFR/server/static/sass/profile-settings.sass b/TFR/server/static/sass/profile-settings.sass new file mode 100644 index 0000000..2d729ed --- /dev/null +++ b/TFR/server/static/sass/profile-settings.sass @@ -0,0 +1,67 @@ +.profile-settings + display: flex + flex-direction: row + gap: 0.5rem + + .picture + margin: 0 + width: 13rem + + position: relative + display: flex + flex-direction: column + + > img + height: 13rem + width: 13rem + + object-fit: cover + + background-color: RGBA($white, 0.02) + border-bottom: 1px solid RGBA($white, 0.1) + border-radius: 2px 2px 0 0 + + transition: opacity 0.1s ease-in-out + + > input + height: 100% + width: 100% + + position: absolute + top: 0 + left: 0 + + opacity: 0 + + > label + padding: 0.5rem 0.7rem + width: 100% + + text-decoration: none + text-align: center + white-space: nowrap + font-size: 0.9em + + background-color: RGBA($white, 0.02) + color: RGB($white) + border-radius: 0 0 2px 2px + + transition: background-color 0.1s ease-in-out + + &:hover + cursor: pointer + + > img + opacity: 0.8 + + > label + background-color: RGBA($white, 0.04) + + .other + width: 100% + height: 100% + + display: flex + flex-direction: column + justify-content: flex-start + gap: 0.5rem diff --git a/TFR/server/static/sass/style.sass b/TFR/server/static/sass/style.sass index f80bb22..fd0004f 100644 --- a/TFR/server/static/sass/style.sass +++ b/TFR/server/static/sass/style.sass @@ -16,6 +16,7 @@ $bronze: var(--bronze) --bronze: 193, 145, 69 @import url('https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700&display=swap') +@import "profile-settings" @import "block" @import "button" @import "hint" diff --git a/TFR/server/templates/macros/input.html b/TFR/server/templates/macros/input.html index f8a7e80..1f44540 100644 --- a/TFR/server/templates/macros/input.html +++ b/TFR/server/templates/macros/input.html @@ -1,15 +1,19 @@ -{% macro text(id, name, type="text", required=False, minlength=0) %} +{% macro text(id, name, type="text", required=False, minlength=0, value="") %} -{% endmacro %} \ No newline at end of file +{% endmacro %} diff --git a/TFR/server/templates/navigation.html b/TFR/server/templates/navigation.html index f2401ec..e349325 100644 --- a/TFR/server/templates/navigation.html +++ b/TFR/server/templates/navigation.html @@ -5,9 +5,9 @@ {% if current_user.is_authenticated %} - + {{ current_user.username }} - {% if not current_user.email %}{% endif %} + {% if not current_user.email %}{% endif %} {% else %} diff --git a/TFR/server/templates/views/account_settings.html b/TFR/server/templates/views/account_settings.html new file mode 100644 index 0000000..1d82ab6 --- /dev/null +++ b/TFR/server/templates/views/account_settings.html @@ -0,0 +1,61 @@ +{% extends "base.html" %} +{% from "macros/input.html" import text %} + +{% block content %} +
Devices and games that you logged into. If you're looking to log out all website users, reset your password instead.
++ | Device | +Created | +Last Used | +
---|---|---|---|
+ | {{ session.device_type }} | +{{ session.created_at.strftime('%Y-%m-%d') }} | +{{ session.last_used.strftime('%Y-%m-%d') }} | +
+ 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? +
+ +Forgotten your current password? Go here [insert password reset tool link]
+ +If you forget your password, you will not be able to recover your account.
-Sample text
-Devices and games that you logged into. If you're looking to logout all website users, reset your password instead.
-- | Device | -Created | -Last Used | -
---|---|---|---|
- | {{ session.device_type }} | -{{ session.created_at.strftime('%Y-%m-%d') }} | -{{ session.last_used.strftime('%Y-%m-%d') }} | -
- | - | - | - |