mirror of
https://github.com/Fluffy-Bean/GameExpo23.git
synced 2025-05-14 14:22:16 +00:00
Merge pull request #1 from Fluffy-Bean/beta
Add more account options Filter highscores more accuratly
This commit is contained in:
commit
412940d455
24 changed files with 465 additions and 217 deletions
|
@ -2,7 +2,6 @@
|
|||
FROM alpine:latest
|
||||
|
||||
EXPOSE 8000
|
||||
# RUN apt update && apt install -y python3 python3-pip postgresql-client
|
||||
RUN apk update && apk add python3 py3-pip postgresql-client
|
||||
WORKDIR /data
|
||||
|
||||
|
|
|
@ -4,17 +4,16 @@ from flask import Flask, render_template, abort
|
|||
from flask_assets import Bundle
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from server.extensions import db, migrate, cache, assets, login_manager
|
||||
from server.models import Users
|
||||
from server.config import MIGRATION_DIR, INSTANCE_DIR
|
||||
from server import views, auth, api, filters
|
||||
from .extensions import db, migrate, cache, assets, login_manager
|
||||
from .models import Users
|
||||
from . import views, account, auth, api, filters
|
||||
|
||||
|
||||
app = Flask(__name__, instance_path=INSTANCE_DIR)
|
||||
app = Flask(__name__)
|
||||
app.config.from_pyfile("config.py")
|
||||
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db, directory=MIGRATION_DIR)
|
||||
migrate.init_app(app, db)
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
@ -37,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)
|
||||
|
|
135
TFR/server/account.py
Normal file
135
TFR/server/account.py
Normal file
|
@ -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")
|
|
@ -5,13 +5,10 @@ from flask import Blueprint, request, jsonify
|
|||
from flask_login import login_required, current_user
|
||||
from werkzeug.security import check_password_hash
|
||||
|
||||
from server.models import Scores, Sessions, Users
|
||||
from server.extensions import db
|
||||
from server.config import (
|
||||
GAME_VERSION,
|
||||
GAME_VERSIONS,
|
||||
from .models import Scores, Sessions, Users
|
||||
from .extensions import db
|
||||
from .config import (
|
||||
GAME_DIFFICULTIES,
|
||||
USER_MAX_TOKENS,
|
||||
MAX_SEARCH_RESULTS,
|
||||
USER_REGEX,
|
||||
)
|
||||
|
@ -23,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
|
||||
|
@ -43,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)
|
||||
|
||||
|
@ -62,7 +59,6 @@ def post():
|
|||
if int(difficulty) not in GAME_DIFFICULTIES:
|
||||
return "Invalid difficulty!"
|
||||
|
||||
|
||||
session_data = Sessions.query.filter_by(auth_key=session_key).first()
|
||||
if not session_data:
|
||||
return "Authentication failed!"
|
||||
|
@ -82,7 +78,7 @@ def post():
|
|||
|
||||
@blueprint.route("/search", methods=["GET"])
|
||||
def search():
|
||||
search_arg = request.args.get("q")
|
||||
search_arg = request.args.get("q").strip()
|
||||
|
||||
if not search_arg:
|
||||
return "No search query provided!", 400
|
||||
|
@ -98,16 +94,15 @@ def search():
|
|||
|
||||
@blueprint.route("/login", methods=["POST"])
|
||||
def login():
|
||||
username = request.form["username"].strip()
|
||||
password = request.form["password"].strip()
|
||||
device = request.form["device"].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)
|
||||
|
||||
if not username or not username_regex.match(username) or not password:
|
||||
return "Username or Password is incorrect!", 400
|
||||
|
||||
user = Users.query.filter_by(username=username).first()
|
||||
|
||||
if not user or not check_password_hash(user.password, password):
|
||||
return "Username or Password is incorrect!", 400
|
||||
|
||||
|
@ -125,10 +120,9 @@ def login():
|
|||
|
||||
@blueprint.route("/authenticate", methods=["POST"])
|
||||
def authenticate():
|
||||
auth_key = request.form["auth_key"].strip()
|
||||
auth_key = request.form.get("session", "").strip()
|
||||
|
||||
session = Sessions.query.filter_by(auth_key=auth_key).first()
|
||||
|
||||
if not session:
|
||||
return "Invalid session", 400
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@ import re
|
|||
import uuid
|
||||
|
||||
from flask import Blueprint, render_template, request, flash, redirect, url_for
|
||||
from flask_login import login_required, login_user, logout_user, current_user
|
||||
from flask_login import login_user
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
from server.extensions import db
|
||||
from server.models import Users, Sessions
|
||||
from server.config import USER_REGEX, USER_EMAIL_REGEX
|
||||
from .extensions import db
|
||||
from .models import Users
|
||||
from .config import USER_REGEX
|
||||
|
||||
|
||||
blueprint = Blueprint("auth", __name__)
|
||||
|
@ -15,29 +15,26 @@ blueprint = Blueprint("auth", __name__)
|
|||
|
||||
@blueprint.route("/auth", methods=["GET"])
|
||||
def auth():
|
||||
return render_template("auth.html")
|
||||
return render_template("views/auth.html")
|
||||
|
||||
|
||||
@blueprint.route("/register", methods=["POST"])
|
||||
def register():
|
||||
# Get the form data
|
||||
username = request.form["username"].strip()
|
||||
email = request.form["email"].strip()
|
||||
password = request.form["password"].strip()
|
||||
username = request.form.get("username", None).strip()
|
||||
password = request.form.get("password", None).strip()
|
||||
confirm = request.form.get("confirm", None).strip()
|
||||
|
||||
username_regex = re.compile(USER_REGEX)
|
||||
email_regex = re.compile(USER_EMAIL_REGEX)
|
||||
error = []
|
||||
|
||||
# Validate the form
|
||||
if not username or not username_regex.match(username):
|
||||
error.append("Username is invalid! Must be alphanumeric, and can contain ._-")
|
||||
if not email or not email_regex.match(email):
|
||||
error.append("Email is invalid! Must be email format")
|
||||
if not password:
|
||||
error.append("Password is empty!")
|
||||
elif len(password) < 8:
|
||||
if not password or len(password) < 8:
|
||||
error.append("Password is too short! Must be at least 8 characters long.")
|
||||
if not confirm or password != confirm:
|
||||
error.append("Passwords do not match!")
|
||||
if Users.query.filter_by(username=username).first():
|
||||
error.append("Username already exists!")
|
||||
|
||||
|
@ -50,7 +47,6 @@ def register():
|
|||
register_user = Users(
|
||||
alt_id=str(uuid.uuid4()),
|
||||
username=username,
|
||||
email=generate_password_hash(email, method="scrypt"),
|
||||
password=generate_password_hash(password, method="scrypt"),
|
||||
)
|
||||
db.session.add(register_user)
|
||||
|
@ -63,10 +59,9 @@ def register():
|
|||
@blueprint.route("/login", methods=["POST"])
|
||||
def login():
|
||||
# Get the form data
|
||||
username = request.form["username"].strip()
|
||||
password = request.form["password"].strip()
|
||||
username = request.form.get("username", None).strip()
|
||||
password = request.form.get("password", None).strip()
|
||||
username_regex = re.compile(USER_REGEX)
|
||||
|
||||
error = []
|
||||
|
||||
# Validate the form
|
||||
|
@ -86,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"))
|
||||
|
|
|
@ -5,7 +5,6 @@ GAME_VERSION = "alpha"
|
|||
GAME_VERSIONS = ["alpha"]
|
||||
GAME_DIFFICULTIES = [0, 1, 2, 3, 4]
|
||||
|
||||
USER_MAX_TOKENS = 3
|
||||
USER_REGEX = r"\b[A-Za-z0-9._-]+\b"
|
||||
USER_EMAIL_REGEX = r"[^@]+@[^@]+\.[^@]+"
|
||||
|
||||
|
@ -24,9 +23,6 @@ SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{user}:{password}@{host}:5432/
|
|||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
SQLALCHEMY_POOL_RECYCLE = 621
|
||||
|
||||
MIGRATION_DIR = "/data/storage/migrations"
|
||||
INSTANCE_DIR = "/data/storage/instance"
|
||||
|
||||
"""
|
||||
# SQLite
|
||||
SECRET_KEY = "dev"
|
||||
|
|
|
@ -2,12 +2,12 @@ import datetime
|
|||
from flask import Blueprint
|
||||
|
||||
|
||||
blueprint = Blueprint('filters', __name__, template_folder='templates')
|
||||
blueprint = Blueprint("filters", __name__, template_folder="templates")
|
||||
|
||||
|
||||
@blueprint.app_template_filter()
|
||||
def format_result(dttm):
|
||||
dttm = str(dttm).split('.')
|
||||
dttm = str(dttm).split(".")
|
||||
time = datetime.timedelta(seconds=int(dttm[0]))
|
||||
microtime = dttm[1][:3]
|
||||
return f'{time}:{microtime}'
|
||||
return f"{time}:{microtime}"
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
"""
|
||||
Database models for the server
|
||||
"""
|
||||
import uuid
|
||||
from flask_login import UserMixin
|
||||
from server.extensions import db
|
||||
from server.config import GAME_VERSION
|
||||
from .extensions import db
|
||||
from .config import GAME_VERSION
|
||||
|
||||
|
||||
class Scores(db.Model):
|
||||
"""
|
||||
Post table
|
||||
Scores supports anonymous posting, and instead just wants to post a score,
|
||||
then the username must be provided. Otherwise, it's grabbed from the user
|
||||
table
|
||||
"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
@ -76,25 +72,6 @@ class PasswordReset(db.Model):
|
|||
)
|
||||
|
||||
|
||||
class Permissions(db.Model):
|
||||
"""
|
||||
Permissions table
|
||||
"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("users.id", use_alter=True))
|
||||
|
||||
user_ban = db.Column(db.Boolean, default=False)
|
||||
user_warn = db.Column(db.Boolean, default=False)
|
||||
|
||||
score_removal = db.Column(db.Boolean, default=False)
|
||||
score_edit = db.Column(db.Boolean, default=False)
|
||||
|
||||
admin_panel = db.Column(db.Boolean, default=False)
|
||||
admin_promote = db.Column(db.Boolean, default=False)
|
||||
admin_demote = db.Column(db.Boolean, default=False)
|
||||
|
||||
|
||||
class ProfileTags(db.Model):
|
||||
"""
|
||||
Profile Tags table
|
||||
|
@ -113,9 +90,12 @@ class Users(db.Model, UserMixin):
|
|||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
alt_id = db.Column(db.String, nullable=False, unique=True)
|
||||
superuser = db.Column(db.Boolean, default=False)
|
||||
|
||||
picture = db.Column(db.String)
|
||||
|
||||
username = db.Column(db.String(32), unique=True, nullable=False)
|
||||
email = db.Column(db.String, unique=True, nullable=False)
|
||||
email = db.Column(db.String)
|
||||
password = db.Column(db.String, nullable=False)
|
||||
|
||||
joined_at = db.Column(
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
min-width: 6rem
|
||||
|
||||
text-decoration: none
|
||||
text-align: end
|
||||
white-space: nowrap
|
||||
font-size: 0.9em
|
||||
|
||||
|
|
79
TFR/server/static/sass/profile-settings.sass
Normal file
79
TFR/server/static/sass/profile-settings.sass
Normal file
|
@ -0,0 +1,79 @@
|
|||
.profile-settings
|
||||
display: flex
|
||||
flex-direction: row
|
||||
gap: 0.5rem
|
||||
|
||||
.picture
|
||||
margin: 0
|
||||
width: 10rem
|
||||
|
||||
position: relative
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
> img
|
||||
height: 10rem
|
||||
width: 10rem
|
||||
|
||||
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
|
||||
@media (max-width: 621px)
|
||||
.profile-settings
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
|
||||
.picture
|
||||
margin: 0 auto
|
||||
width: 13rem
|
||||
|
||||
> img
|
||||
height: 13rem
|
||||
width: 13rem
|
|
@ -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"
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="block">
|
||||
<h2>Login</h2>
|
||||
<p>Welcome back!</p>
|
||||
<form action="{{ url_for('auth.login') }}" method="POST">
|
||||
<span class="text-input">
|
||||
<label for="login-username">Username</label>
|
||||
<input type="text" name="username" id="login-username" required>
|
||||
</span>
|
||||
|
||||
<span class="text-input">
|
||||
<label for="login-password">Password</label>
|
||||
<input type="password" name="password" id="login-password" required>
|
||||
</span>
|
||||
|
||||
<button type="submit" class="button primary">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h2>Register</h2>
|
||||
<p>Don't have an account?</p>
|
||||
<form action="{{ url_for('auth.register') }}" method="POST">
|
||||
<span class="text-input">
|
||||
<label for="register-username">Username</label>
|
||||
<input type="text" name="username" id="register-username" required>
|
||||
</span>
|
||||
|
||||
<span class="text-input">
|
||||
<label for="register-email">Email</label>
|
||||
<input type="text" name="email" id="register-email" required>
|
||||
</span>
|
||||
|
||||
<span class="text-input">
|
||||
<label for="register-password">Password</label>
|
||||
<input type="password" name="password" id="register-password" required>
|
||||
</span>
|
||||
|
||||
<button type="submit" class="button primary">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Front Rooms Highscores</title>
|
||||
<title>{% block title %}Front Rooms Highscores{% endblock %}</title>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
@ -16,10 +16,9 @@
|
|||
{% assets "scripts" %}<script src="{{ ASSET_URL }}"></script>{% endassets %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="search-hint">
|
||||
<p>Start typing to see results...</p>
|
||||
</div>
|
||||
<div class="search-hint"><p>Start typing to see results...</p></div>
|
||||
|
||||
<!-- Hopefully I can make this change seasonally/weekly/something-ly -->
|
||||
<picture class="background">
|
||||
<source srcset="{{ url_for('static', filename='images/background.webp') }}">
|
||||
<img src="{{ url_for('static', filename='images/background.png') }}" alt="The Front Rooms Level select render">
|
||||
|
@ -28,35 +27,30 @@
|
|||
<div class="app">
|
||||
<!-- Get flashed lol -->
|
||||
<div class="flash">
|
||||
<!-- My bad -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<p class="{{ category }}" onclick="this.remove()"><span><i class="ph-bold ph-x"></i></span>{{ message }}</p>
|
||||
<p class="{{ category }}" onclick="this.remove()">
|
||||
<span><i class="ph-bold ph-x"></i></span>
|
||||
{{ message }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<!-- Overcomplicated header, probably should just use CSS for some of this stuff -->
|
||||
<picture class="title">
|
||||
<source srcset="{{ url_for('static', filename='images/title.webp') }}">
|
||||
<img src="{{ url_for('static', filename='images/title.png') }}" alt="The Front Rooms logo">
|
||||
</picture>
|
||||
|
||||
<nav>
|
||||
<a href="{{ url_for('views.index') }}" class="button">Scores</a>
|
||||
<a href="{{ url_for('views.about') }}" class="button">About</a>
|
||||
<!-- Import navigation bar -->
|
||||
{% include 'navigation.html' %}
|
||||
|
||||
<span class="spacer"></span>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('views.settings') }}" class="button primary">{{ current_user.username }}</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.auth') }}" class="button primary"><i class="ph ph-user-circle"></i></a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
<!-- Secondary nav bar for page specific content -->
|
||||
<!-- Second nav for page specific tools -->
|
||||
{% block nav %}{% endblock %}
|
||||
</header>
|
||||
|
||||
|
|
19
TFR/server/templates/macros/input.html
Normal file
19
TFR/server/templates/macros/input.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% macro text(id, name, type="text", required=False, minlength=0, value="") %}
|
||||
<span class="text-input">
|
||||
<label for="{{ id }}">
|
||||
{{ name|title }}
|
||||
{% if required %}
|
||||
<span style="color: rgb(var(--secondary)) !important;">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="{{ type }}"
|
||||
name="{{ name }}"
|
||||
id="{{ id }}"
|
||||
value="{{ value }}"
|
||||
{% if required %}required{% endif %}
|
||||
minlength="{{ minlength }}"
|
||||
>
|
||||
</span>
|
||||
{% endmacro %}
|
15
TFR/server/templates/navigation.html
Normal file
15
TFR/server/templates/navigation.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<nav>
|
||||
<a href="{{ url_for('views.index') }}" class="button">Scores</a>
|
||||
<a href="{{ url_for('views.about') }}" class="button">About</a>
|
||||
|
||||
<span class="spacer"></span>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('account.settings') }}" class="button primary">
|
||||
{{ current_user.username }}
|
||||
{% if not current_user.email %}<i class="ph ph-warning" style="margin-left: 0.2rem !important;"></i>{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.auth') }}" class="button primary"><i class="ph ph-user-circle"></i></a>
|
||||
{% endif %}
|
||||
</nav>
|
|
@ -1,47 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="block">
|
||||
<h2>Hello, {{ current_user.username }}!</h2>
|
||||
<p>Sample text</p>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h2>Sessions</h2>
|
||||
<p>Devices and games that you logged into. If you're looking to logout all website users, reset your password instead.</p>
|
||||
<div class="table">
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Device</th>
|
||||
<th>Created</th>
|
||||
<th>Last Used</th>
|
||||
</tr>
|
||||
{% if sessions %}
|
||||
{% for session in sessions %}
|
||||
<tr id="sess-{{ session.id }}">
|
||||
<td><button onclick="yeetSession({{ session.id }})" class="button secondary"><i class="ph ph-trash"></i></button></td>
|
||||
<td>{{ session.device_type }}</td>
|
||||
<td>{{ session.created_at.strftime('%Y-%m-%d') }}</td>
|
||||
<td>{{ session.last_used.strftime('%Y-%m-%d') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block secondary">
|
||||
<h2>Danger Zone</h2>
|
||||
<p>Be careful!</p>
|
||||
<a href="{{ url_for('views.settings', action='delete') }}" class="button secondary" style="margin-bottom: 0.5rem">Delete Account</a>
|
||||
<a href="{{ url_for('views.settings', action='password') }}" class="button secondary" style="margin-bottom: 0.5rem">Reset Password</a>
|
||||
<a href="{{ url_for('views.settings', action='logout') }}" class="button secondary">Logout</a>
|
||||
</div>
|
||||
{% endblock %}
|
61
TFR/server/templates/views/account_settings.html
Normal file
61
TFR/server/templates/views/account_settings.html
Normal file
|
@ -0,0 +1,61 @@
|
|||
{% extends "base.html" %}
|
||||
{% from "macros/input.html" import text %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block">
|
||||
<h2 style="margin-bottom: 1rem;">Profile Settings</h2>
|
||||
<form action="{{ url_for('account.settings') }}" method="POST">
|
||||
<div class="profile-settings">
|
||||
<div class="picture">
|
||||
<img src="{{ url_for('static', filename='images/error/2.jpg') }}" alt="Profile picture">
|
||||
<label for="profile-picture">Profile Picture</label>
|
||||
<input type="file" name="picture" id="profile-picture">
|
||||
</div>
|
||||
<div class="other">
|
||||
{{ 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) }}
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="button primary">Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h2>Sessions</h2>
|
||||
<p>Devices and games that you logged into. If you're looking to log out all website users, reset your password instead.</p>
|
||||
<div class="table">
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Device</th>
|
||||
<th>Created</th>
|
||||
<th>Last Used</th>
|
||||
</tr>
|
||||
{% for session in sessions %}
|
||||
<tr id="sess-{{ session.id }}">
|
||||
<td><button onclick="yeetSession({{ session.id }})" class="button secondary"><i class="ph ph-trash"></i></button></td>
|
||||
<td>{{ session.device_type }}</td>
|
||||
<td>{{ session.created_at.strftime('%Y-%m-%d') }}</td>
|
||||
<td>{{ session.last_used.strftime('%Y-%m-%d') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block secondary">
|
||||
<h2>Danger Zone</h2>
|
||||
<p>Be careful!</p>
|
||||
<a href="{{ url_for('account.delete_account') }}" class="button secondary" style="margin-bottom: 0.5rem">Delete Account</a>
|
||||
<a href="{{ url_for('account.password_reset') }}" class="button secondary" style="margin-bottom: 0.5rem">Reset Password</a>
|
||||
<a href="{{ url_for('account.settings', action='logout') }}" class="button secondary">Logout</a>
|
||||
</div>
|
||||
|
||||
|
||||
{% if not current_user.email %}
|
||||
<script>
|
||||
addFlashMessage("No Email set. If you loose your account, it will not be possible to recover it!", "error")
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
25
TFR/server/templates/views/auth.html
Normal file
25
TFR/server/templates/views/auth.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
{% extends "base.html" %}
|
||||
{% from "macros/input.html" import text %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block">
|
||||
<h2>Login</h2>
|
||||
<p>Welcome back!</p>
|
||||
<form action="{{ url_for('auth.login') }}" method="POST">
|
||||
{{ text(id="login-username", name="username", required=True) }}
|
||||
{{ text(id="login-password", name="password", type="password", required=True) }}
|
||||
<button type="submit" class="button primary">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h2>Register</h2>
|
||||
<p>Don't have an account?</p>
|
||||
<form action="{{ url_for('auth.register') }}" method="POST">
|
||||
{{ text(id="register-username", name="username", required=True) }}
|
||||
{{ text(id="register-password", name="password", type="password", required=True, minlength=8) }}
|
||||
{{ text(id="register-confirm", name="confirm", type="password", required=True, minlength=8) }}
|
||||
<button type="submit" class="button primary">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
17
TFR/server/templates/views/delete_account.html
Normal file
17
TFR/server/templates/views/delete_account.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
{% extends "base.html" %}
|
||||
{% from "macros/input.html" import text %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block secondary">
|
||||
<h2>Delete account</h2>
|
||||
<p>
|
||||
Deleting your account will delete <span style="color: rgb(var(--secondary)) !important;">EVERYTHING</span> on your account, including <span style="color: rgb(var(--secondary)) !important;">ALL</span> your ever submitted scores.
|
||||
There is <span style="color: rgb(var(--secondary)) !important;">NO WAY</span> to recover your account from this, are you sure you want todo this?
|
||||
</p>
|
||||
<form action="{{ url_for('account.delete_account') }}" method="POST">
|
||||
{{ text(id="username", name="username", required=True) }}
|
||||
{{ text(id="password", name="password", type="password", required=True) }}
|
||||
<button type="submit" class="button secondary">Delete account forever</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
15
TFR/server/templates/views/reset_password.html
Normal file
15
TFR/server/templates/views/reset_password.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends "base.html" %}
|
||||
{% from "macros/input.html" import text %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block secondary">
|
||||
<h2>Password Reset</h2>
|
||||
<p>Forgotten your current password? Go here [insert password reset tool link]</p>
|
||||
<form action="{{ url_for('account.password_reset') }}" method="POST">
|
||||
{{ 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) }}
|
||||
<button type="submit" class="button secondary">Reset</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -22,7 +22,13 @@
|
|||
|
||||
<span class="text-input">
|
||||
<label for="search">Username</label>
|
||||
<input type="text" name="user" id="search" {% if user %}value="{{ user }}"{% endif %} autocomplete="off"/>
|
||||
<input
|
||||
type="text"
|
||||
name="user"
|
||||
id="search"
|
||||
{% if user %}value="{{ user.username }}"{% endif %}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<button class="button"><i class="ph ph-magnifying-glass"></i></button>
|
||||
|
@ -30,6 +36,12 @@
|
|||
</nav>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% if user %}
|
||||
<div class="block">
|
||||
<h2 style="margin-bottom: 0;">{{ user.username }}</h2>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if scores %}
|
||||
<div class="table">
|
||||
<table>
|
|
@ -1,7 +1,9 @@
|
|||
from flask import Blueprint, request, render_template, abort, flash, redirect, url_for
|
||||
from flask_login import login_required, current_user, logout_user
|
||||
from server.models import Scores, Users, Sessions
|
||||
from server.config import GAME_VERSION, MAX_TOP_SCORES
|
||||
from flask import Blueprint, request, render_template, abort
|
||||
from sqlalchemy import func
|
||||
|
||||
from .models import Scores, Users
|
||||
from .config import GAME_VERSION, MAX_TOP_SCORES
|
||||
from .extensions import db
|
||||
|
||||
|
||||
blueprint = Blueprint("views", __name__)
|
||||
|
@ -10,45 +12,44 @@ blueprint = Blueprint("views", __name__)
|
|||
@blueprint.route("/")
|
||||
def index():
|
||||
diff_arg = request.args.get("diff", 0)
|
||||
ver_arg = request.args.get("ver", GAME_VERSION)
|
||||
user_arg = request.args.get("user", None)
|
||||
ver_arg = request.args.get("ver", GAME_VERSION).strip()
|
||||
user_arg = request.args.get("user", "").strip()
|
||||
user = None
|
||||
|
||||
scores = Scores.query.filter_by(difficulty=diff_arg)
|
||||
scores = db.session.query(Scores).filter_by(difficulty=diff_arg)
|
||||
|
||||
subquery = (
|
||||
db.session.query(Scores.user_id, func.min(Scores.score).label('min'))
|
||||
.group_by(Scores.user_id)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
if ver_arg:
|
||||
scores = scores.filter_by(version=ver_arg)
|
||||
if user_arg:
|
||||
if user := Users.query.filter_by(username=user_arg).first():
|
||||
|
||||
if not user_arg:
|
||||
scores = (
|
||||
scores.join(subquery, Scores.user_id == subquery.c.user_id)
|
||||
.filter(Scores.score == subquery.c.min)
|
||||
)
|
||||
else:
|
||||
user = Users.query.filter_by(username=user_arg).first()
|
||||
if user:
|
||||
scores = scores.filter_by(user_id=user.id)
|
||||
print(user.id)
|
||||
else:
|
||||
abort(404, "User not found")
|
||||
|
||||
scores = scores.order_by(Scores.score.asc()).limit(MAX_TOP_SCORES).all()
|
||||
|
||||
return render_template(
|
||||
"scores.html", scores=scores, diff=int(diff_arg), ver=ver_arg, user=user_arg
|
||||
"views/scores.html",
|
||||
scores=scores,
|
||||
diff=int(diff_arg),
|
||||
ver=ver_arg,
|
||||
user=user
|
||||
)
|
||||
|
||||
|
||||
@blueprint.route("/about")
|
||||
def about():
|
||||
return render_template("about.html")
|
||||
|
||||
|
||||
@blueprint.route("/settings", methods=["GET"])
|
||||
@login_required
|
||||
def settings():
|
||||
action = request.args.get("action", None)
|
||||
|
||||
if action == "logout":
|
||||
logout_user()
|
||||
flash("Successfully logged out!", "success")
|
||||
return redirect(url_for("views.index"))
|
||||
if action == "delete":
|
||||
flash("Insert delete function", "error")
|
||||
if action == "password":
|
||||
flash("Insert password change function", "error")
|
||||
|
||||
sessions = Sessions.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template("settings.html", sessions=sessions)
|
||||
return render_template("views/about.html")
|
||||
|
|
|
@ -36,8 +36,7 @@ services:
|
|||
build: TFR
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./TFR/storage:/data/storage
|
||||
- ./TFR/logs:/data/logs
|
||||
- ./TFR/storage/migrations:/data/migrations
|
||||
environment:
|
||||
FLASK_KEY: ${THE_FRONT_ROOMS_SECRETE_KEY}
|
||||
DB_USER: ${POSTGRES_USER}
|
||||
|
|
Loading…
Add table
Reference in a new issue