diff --git a/Caddy/Caddyfile b/Caddy/Caddyfile index b7a9dd1..b8b54af 100644 --- a/Caddy/Caddyfile +++ b/Caddy/Caddyfile @@ -1,4 +1,8 @@ {$THE_FRONT_ROOMS_DOMAIN} -reverse_proxy tfr:8000 -encode gzip +reverse_proxy tfr:8000 { + header_up X-Forwarded-For {http.request.remote.addr} + header_up X-Real-IP {http.request.remote.addr} +} + +encode gzip diff --git a/TFR/server/account.py b/TFR/server/account.py index badb41b..fdd3235 100644 --- a/TFR/server/account.py +++ b/TFR/server/account.py @@ -1,5 +1,6 @@ import uuid import os +import re from PIL import Image from flask import Blueprint, request, render_template, flash, redirect, url_for @@ -14,7 +15,7 @@ from .config import ( UPLOAD_DIR, UPLOAD_RESOLUTION, ) -from .models import Users, Sessions, Scores, ProfileTags, PasswordReset +from .models import Users, Sessions, Scores from .extensions import db @@ -38,16 +39,28 @@ def get_settings(): @blueprint.route("/settings", methods=["POST"]) @login_required def post_settings(): + # This is the worst part of this entire project + # This gotta go :sobbing: username = request.form.get("username", "").strip() email = request.form.get("email", "").strip() password = request.form.get("password", "").strip() + + discord = request.form.get("discord", "").strip() + twitter = request.form.get("twitter", "").strip() + twitch = request.form.get("twitch", "").strip() + youtube = request.form.get("youtube", "").strip() + + twitter_regex = re.compile(r"^(?!.*\.\.)(?!.*\.$)[\w.]{1,15}$") + twitch_regex = re.compile("^(?=.{4,25}$)(?!_)(?!.*[_.]{2})[a-zA-Z0-9._]+$") + youtube_regex = re.compile("^(?!.*[._]{2})[a-zA-Z0-9._-]{1,50}$") + 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")) + return redirect(url_for("account.get_settings")) if "file" in request.files and request.files["file"].filename: picture = request.files["file"] @@ -96,15 +109,33 @@ def post_settings(): else: error.append("Email is invalid!") + if discord: + user.discord = discord + if twitter: + if twitter_regex.match(twitter): + user.twitter = twitter + else: + error.append("Twitter is invalid!") + if twitch: + if twitch_regex.match(twitch): + user.twitch = twitch + else: + error.append("Twitch is invalid!") + if youtube: + if youtube_regex.match(youtube): + user.youtube = youtube + else: + error.append("YouTube is invalid!") + if error: for err in error: flash(err, "error") - return redirect(url_for("account.settings")) + return redirect(url_for("account.get_settings")) db.session.commit() flash("Successfully updated account!", "success") - return redirect(url_for("account.settings")) + return redirect(url_for("account.get_settings")) @blueprint.route("/password", methods=["GET"]) @@ -175,8 +206,6 @@ def post_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() diff --git a/TFR/server/api.py b/TFR/server/api.py index 8bcd790..edf645b 100644 --- a/TFR/server/api.py +++ b/TFR/server/api.py @@ -61,26 +61,26 @@ def post(): return "Score is not valid!" try: - float(score) - int(difficulty) + score = float(score) + difficulty = int(difficulty) except TypeError: return "Invalid score and difficulty must be valid numbers!" - if int(difficulty) not in GAME_DIFFICULTIES: + if difficulty not in GAME_DIFFICULTIES: return "Invalid difficulty!" if version not in GAME_VERSIONS: return "Invalid version!" # This is a fix for a bug in the game that we dunno how to actually fix - # if score < 10: - # return "Score is impossible!" + if score < 10: + return "Score is impossible!" session_data = Sessions.query.filter_by(auth_key=session_key).first() if not session_data: return "Authentication failed!" score_upload = Scores( - score=float(score), - difficulty=int(difficulty), + score=score, + difficulty=difficulty, version=version, user_id=session_data.user_id, ) diff --git a/TFR/server/auth.py b/TFR/server/auth.py index e880801..0e6d548 100644 --- a/TFR/server/auth.py +++ b/TFR/server/auth.py @@ -77,4 +77,4 @@ def login(): login_user(user, remember=True) flash("Successfully logged in!", "success") - return redirect(url_for("account.settings")) + return redirect(url_for("account.get_settings")) diff --git a/TFR/server/config.py b/TFR/server/config.py index 9700d34..d92a7bf 100644 --- a/TFR/server/config.py +++ b/TFR/server/config.py @@ -39,3 +39,5 @@ port = 5432 SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{user}:{password}@{host}:{port}/{db}" SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_POOL_RECYCLE = 621 + +LOGS_DIR = "/data/logs" diff --git a/TFR/server/extensions.py b/TFR/server/extensions.py index 9916969..f1f8a30 100644 --- a/TFR/server/extensions.py +++ b/TFR/server/extensions.py @@ -1,9 +1,21 @@ +import logging +from os import path + from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_assets import Environment from flask_caching import Cache from flask_login import LoginManager +from .config import LOGS_DIR + +logger = logging +logger.getLogger(path.join(LOGS_DIR, "server.log")) +logger.basicConfig( + level=logging.DEBUG, + format="%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s", +) + db = SQLAlchemy() migrate = Migrate() assets = Environment() diff --git a/TFR/server/models.py b/TFR/server/models.py index 70b6551..04d6485 100644 --- a/TFR/server/models.py +++ b/TFR/server/models.py @@ -55,32 +55,28 @@ class Sessions(db.Model): ) -class PasswordReset(db.Model): +class TagJunction(db.Model): """ - Password reset table + Tag Junction table """ id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id", use_alter=True)) - - reset_key = db.Column(db.String, nullable=False, unique=True) - - created_at = db.Column( - db.DateTime, - nullable=False, - server_default=db.func.now(), - ) + tag_id = db.Column(db.Integer, db.ForeignKey("tags.id", use_alter=True)) -class ProfileTags(db.Model): +class Tags(db.Model): """ Profile Tags table """ id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey("users.id", use_alter=True)) + users = db.relationship("TagJunction", backref=db.backref("tags", lazy=True)) tag = db.Column(db.String, nullable=False) + icon = db.Column(db.String) + color = db.Column(db.String) class Users(db.Model, UserMixin): @@ -98,6 +94,11 @@ class Users(db.Model, UserMixin): email = db.Column(db.String) password = db.Column(db.String, nullable=False) + discord = db.Column(db.String) + twitter = db.Column(db.String) + twitch = db.Column(db.String) + youtube = db.Column(db.String) + joined_at = db.Column( db.DateTime, nullable=False, @@ -106,8 +107,7 @@ class Users(db.Model, UserMixin): scores = db.relationship("Scores", backref=db.backref("users", lazy=True)) tokens = db.relationship("Sessions", backref=db.backref("users", lazy=True)) - reset = db.relationship("PasswordReset", backref=db.backref("users", lazy=True)) - tags = db.relationship("ProfileTags", backref=db.backref("users", lazy=True)) + tags = db.relationship("TagJunction", backref=db.backref("users", lazy=True)) def get_id(self): return str(self.alt_id) diff --git a/TFR/server/static/js/search.js b/TFR/server/static/js/search.js index 4cc1638..bed45aa 100644 --- a/TFR/server/static/js/search.js +++ b/TFR/server/static/js/search.js @@ -54,7 +54,8 @@ function getSearch() { el.innerHTML = user; el.onmousedown = function (event) { event.preventDefault(); - search = user.toString(); + search = document.querySelector('#search'); + search.value = user.toString(); search.blur(); } hint.appendChild(el); diff --git a/TFR/server/static/sass/block.sass b/TFR/server/static/sass/block.sass index c40accd..7ff7126 100644 --- a/TFR/server/static/sass/block.sass +++ b/TFR/server/static/sass/block.sass @@ -60,11 +60,6 @@ gap: 0.5rem - h2 - margin: 0 - font-size: 1.3rem - color: RGB($white) - p margin: 0 font-size: 1rem @@ -76,11 +71,82 @@ border: 0 solid transparent border-bottom: 1px solid RGBA($white, 0.1) -@media (max-width: 621px) - .account-block - flex-direction: column + .profile-title + display: flex + flex-direction: row + justify-content: space-between + gap: 0.5rem - > img + h2 + margin: 0 + font-size: 1.3rem + color: RGB($white) + + .profile-tag + margin: auto 0 + padding: 0.2rem 0.3rem + + font-size: 0.8rem + + color: RGB($black) + background-color: RGB($white) + border-radius: 2px + + > img + width: 0.9rem + height: 0.9rem + margin-right: 0.2rem + + .profile-links + display: flex + flex-direction: row + flex-wrap: wrap + gap: 0.5rem + + > .discord, > .twitter, > .twitch, > .youtube + margin: 0 + padding: 0.2rem 0.3rem + + width: 2rem + height: 2rem + + display: flex + flex-direction: row + align-items: center + justify-content: center + + text-decoration: none + + color: RGB($white) + border-radius: 2px + border: 0 solid transparent + + > i + font-size: 1.3rem + + &:hover + cursor: pointer + filter: brightness(1.2) + + > .discord + background-color: #5865F2 + + > .twitter + background-color: #1DA1F2 + + > .twitch + background-color: #9146FF + + > .youtube + background-color: #FF0000 + color: RGB($white) + + +@media (max-width: 550px) + .account-block + // flex-direction: column + + > img margin: 0 auto - width: 15rem - height: 15rem + width: 5rem + height: 5rem diff --git a/TFR/server/static/sass/profile-settings.sass b/TFR/server/static/sass/profile-settings.sass index 4b613ac..4b1c0ca 100644 --- a/TFR/server/static/sass/profile-settings.sass +++ b/TFR/server/static/sass/profile-settings.sass @@ -4,7 +4,7 @@ gap: 1rem .picture - margin: 0 + margin: 0 0 auto 0 width: 10rem position: relative diff --git a/TFR/server/static/sass/style.sass b/TFR/server/static/sass/style.sass index 716e30b..ecaebfe 100644 --- a/TFR/server/static/sass/style.sass +++ b/TFR/server/static/sass/style.sass @@ -52,7 +52,7 @@ body margin: 0 auto padding: 0 - width: 800px + width: 900px min-height: 100vh position: relative diff --git a/TFR/server/templates/macros/input.html b/TFR/server/templates/macros/input.html index 1f44540..b81d14c 100644 --- a/TFR/server/templates/macros/input.html +++ b/TFR/server/templates/macros/input.html @@ -11,7 +11,7 @@ type="{{ type }}" name="{{ name }}" id="{{ id }}" - value="{{ value }}" + {% if value %}value="{{ value }}"{% endif %} {% if required %}required{% endif %} minlength="{{ minlength }}" > diff --git a/TFR/server/templates/views/account_settings.html b/TFR/server/templates/views/account_settings.html index e92eda7..a53646e 100644 --- a/TFR/server/templates/views/account_settings.html +++ b/TFR/server/templates/views/account_settings.html @@ -18,6 +18,9 @@
{{ text(id="profile-username", name="username", value=current_user.username) }} {{ text(id="profile-email", name="email") }} + + + {{ text(id="profile-password", name="password", type="password", required=True, minlength=8) }} @@ -28,6 +31,28 @@
+
+

Linked Socials

+
+
+
+ {{ text(id="socials-discord", name="discord", value=current_user.discord) }} + {{ text(id="socials-twitter", name="twitter", value=current_user.twitter) }} + {{ text(id="socials-twitch", name="twitch", value=current_user.twitch) }} + {{ text(id="socials-youtube", name="youtube", value=current_user.youtube) }} + + + + {{ text(id="socials-password", name="password", type="password", required=True, minlength=8) }} + + + + +
+
+
+
+

Sessions

Devices and games that you logged into. If you're looking to log out all website users, reset your password instead.

diff --git a/TFR/server/templates/views/scores.html b/TFR/server/templates/views/scores.html index 3580bb3..51c9e7e 100644 --- a/TFR/server/templates/views/scores.html +++ b/TFR/server/templates/views/scores.html @@ -42,10 +42,23 @@ {% else %} Profile picture {% endif %} +
-

{{ user.username }}

-
+
+

{{ user.username }}

+ {% for tag in tags %}{{ tag.tag }}{% endfor %} +
+

Joined {{ user.joined_at|timesince }}

+ +
+ +
{% endif %} diff --git a/TFR/server/views.py b/TFR/server/views.py index 5dfdf78..00d65bb 100644 --- a/TFR/server/views.py +++ b/TFR/server/views.py @@ -1,7 +1,7 @@ from flask import Blueprint, request, render_template, abort from sqlalchemy import func -from .models import Scores, Users +from .models import Scores, Users, TagJunction, Tags from .config import GAME_VERSION, MAX_TOP_SCORES from .extensions import db @@ -15,6 +15,7 @@ def index(): ver_arg = request.args.get("ver", GAME_VERSION).strip() user_arg = request.args.get("user", "").strip() user = None + tags = None scores = db.session.query(Scores).filter_by(difficulty=diff_arg) @@ -33,6 +34,13 @@ def index(): ) else: user = Users.query.filter_by(username=user_arg).first() + # get all tags from the junction table and add them to a list of tags + tags = ( + db.session.query(Tags) + .join(TagJunction) + .filter(TagJunction.user_id == user.id) + .all() + ) if user: scores = scores.filter_by(user_id=user.id) else: @@ -41,7 +49,12 @@ def index(): scores = scores.order_by(Scores.score.asc()).limit(MAX_TOP_SCORES).all() return render_template( - "views/scores.html", scores=scores, diff=int(diff_arg), ver=ver_arg, user=user + "views/scores.html", + scores=scores, + diff=int(diff_arg), + ver=ver_arg, + user=user, + tags=tags, ) diff --git a/docker-compose.yml b/docker-compose.yml index 7fde659..36aa71a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,7 @@ services: volumes: - ./TFR/storage/migrations:/data/migrations - ./TFR/storage/uploads:/data/uploads + - ./TFR/storage/logs:/data/logs environment: FLASK_KEY: ${THE_FRONT_ROOMS_SECRETE_KEY} DB_USER: ${POSTGRES_USER}