|
@ -11,3 +11,4 @@ Flask-Caching
|
|||
libsass-bin
|
||||
jsmin
|
||||
cssmin
|
||||
timeago
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from random import randint
|
||||
|
||||
from flask import Flask, render_template, abort
|
||||
from flask_assets import Bundle
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
@ -53,8 +51,6 @@ def error_page(err):
|
|||
abort(500)
|
||||
|
||||
return (
|
||||
render_template(
|
||||
"error.html", error=err.code, msg=err.description, image=str(randint(1, 3))
|
||||
),
|
||||
render_template("error.html", error=err.code, msg=err.description),
|
||||
err.code,
|
||||
)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import uuid
|
||||
import re
|
||||
import os
|
||||
from PIL import Image
|
||||
|
||||
|
@ -29,9 +28,6 @@ def settings():
|
|||
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()
|
||||
|
@ -42,7 +38,7 @@ def settings():
|
|||
|
||||
if "file" in request.files and request.files['file'].filename:
|
||||
picture = request.files["file"]
|
||||
file_ext = picture.filename.split(".")[-1]
|
||||
file_ext = picture.filename.split(".")[-1].lower()
|
||||
file_name = f"{user.id}.{file_ext}"
|
||||
|
||||
if file_ext not in UPLOAD_EXTENSIONS:
|
||||
|
@ -51,11 +47,14 @@ def settings():
|
|||
error.append(f"Picture must be less than {UPLOAD_EXTENSIONS / 1000000}MB!")
|
||||
|
||||
image = Image.open(picture.stream)
|
||||
image_x, image_y = image.size
|
||||
image.thumbnail((
|
||||
min(image_x, UPLOAD_RESOLUTION),
|
||||
min(image_y, UPLOAD_RESOLUTION)
|
||||
))
|
||||
|
||||
# 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:
|
||||
|
@ -66,16 +65,21 @@ def settings():
|
|||
os.remove(os.path.join(UPLOAD_DIR, user.picture))
|
||||
|
||||
user.picture = file_name
|
||||
image.save(os.path.join(UPLOAD_DIR, 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):
|
||||
if USER_REGEX.match(username):
|
||||
user.username = username
|
||||
else:
|
||||
error.append("Username is invalid!")
|
||||
if email:
|
||||
if email_regex.match(email):
|
||||
if USER_EMAIL_REGEX.match(email):
|
||||
user.email = email
|
||||
else:
|
||||
error.append("Email is invalid!")
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import re
|
||||
import shortuuid
|
||||
|
||||
from flask import Blueprint, request, jsonify, send_from_directory
|
||||
|
@ -9,6 +8,9 @@ from werkzeug.utils import secure_filename
|
|||
from .models import Scores, Sessions, Users
|
||||
from .extensions import db
|
||||
from .config import (
|
||||
GAME_VERSION,
|
||||
GAME_VERSIONS,
|
||||
GAME_DIFFICULTY,
|
||||
GAME_DIFFICULTIES,
|
||||
MAX_SEARCH_RESULTS,
|
||||
USER_REGEX,
|
||||
|
@ -49,8 +51,8 @@ def tokens():
|
|||
@blueprint.route("/post", methods=["POST"])
|
||||
def post():
|
||||
session_key = request.form.get("session", "").strip()
|
||||
version = request.form.get("version", "alpha").strip()
|
||||
difficulty = request.form.get("difficulty", 0)
|
||||
version = request.form.get("version", GAME_VERSION).strip()
|
||||
difficulty = request.form.get("difficulty", GAME_DIFFICULTY)
|
||||
score = request.form.get("score", 0)
|
||||
|
||||
if not session_key:
|
||||
|
@ -66,6 +68,8 @@ def post():
|
|||
|
||||
if int(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!"
|
||||
|
@ -110,9 +114,8 @@ def login():
|
|||
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:
|
||||
if not username or not USER_REGEX.match(username) or not password:
|
||||
return "Username or Password is incorrect!", 400
|
||||
|
||||
user = Users.query.filter_by(username=username).first()
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import re
|
||||
import uuid
|
||||
|
||||
from flask import Blueprint, render_template, request, flash, redirect, url_for
|
||||
|
@ -24,12 +23,10 @@ def register():
|
|||
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)
|
||||
error = []
|
||||
|
||||
# Validate the form
|
||||
if not username or not username_regex.match(username):
|
||||
if not username or not USER_REGEX.match(username):
|
||||
error.append("Username is invalid! Must be alphanumeric, and can contain ._-")
|
||||
if not password or len(password) < 8:
|
||||
error.append("Password is too short! Must be at least 8 characters long.")
|
||||
|
@ -61,11 +58,10 @@ def login():
|
|||
# Get the form data
|
||||
username = request.form.get("username", None).strip()
|
||||
password = request.form.get("password", None).strip()
|
||||
username_regex = re.compile(USER_REGEX)
|
||||
error = []
|
||||
|
||||
# Validate the form
|
||||
if not username or not username_regex.match(username) or not password:
|
||||
if not username or not USER_REGEX.match(username) or not password:
|
||||
error.append("Username or Password is incorrect!")
|
||||
|
||||
user = Users.query.filter_by(username=username).first()
|
||||
|
|
|
@ -1,35 +1,41 @@
|
|||
from os import getenv
|
||||
import re
|
||||
|
||||
|
||||
SECRET_KEY = getenv("FLASK_KEY")
|
||||
|
||||
UPLOAD_DIR = "/data/uploads"
|
||||
UPLOAD_EXTENSIONS = ["png", "jpg", "jpeg", "gif"]
|
||||
UPLOAD_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp"]
|
||||
UPLOAD_RESOLUTION = 512
|
||||
UPLOAD_MAX_SIZE = 3 * 1024 * 1024 # 3MB
|
||||
|
||||
GAME_VERSION = "alpha"
|
||||
GAME_VERSIONS = ["alpha"]
|
||||
GAME_DIFFICULTIES = [0, 1, 2, 3, 4]
|
||||
GAME_DIFFICULTY = 0
|
||||
|
||||
USER_REGEX = r"\b[A-Za-z0-9._-]+\b"
|
||||
USER_EMAIL_REGEX = r"[^@]+@[^@]+\.[^@]+"
|
||||
GAME_VERSIONS = {
|
||||
"alpha": "Alpha",
|
||||
"alpha-expo": "Alpha (Expo Build)",
|
||||
}
|
||||
GAME_DIFFICULTIES = {
|
||||
0: "Easy - Level 1",
|
||||
1: "Easy - Level 2",
|
||||
2: "Easy - Level 3",
|
||||
3: "Medium",
|
||||
4: "Hard",
|
||||
}
|
||||
|
||||
USER_REGEX = re.compile(r"\b[A-Za-z0-9._-]+\b")
|
||||
USER_EMAIL_REGEX = re.compile(r"[^@]+@[^@]+\.[^@]+")
|
||||
|
||||
MAX_TOP_SCORES = 15
|
||||
MAX_SEARCH_RESULTS = 5
|
||||
|
||||
# Postgres
|
||||
SECRET_KEY = getenv("FLASK_KEY")
|
||||
|
||||
user = getenv("DB_USER")
|
||||
password = getenv("DB_PASSWORD")
|
||||
host = getenv("DB_HOST")
|
||||
db = getenv("DB_NAME")
|
||||
port = 5432
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{user}:{password}@{host}:5432/{db}"
|
||||
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{user}:{password}@{host}:{port}/{db}"
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
SQLALCHEMY_POOL_RECYCLE = 621
|
||||
|
||||
"""
|
||||
# SQLite
|
||||
SECRET_KEY = "dev"
|
||||
SQLALCHEMY_DATABASE_URI = "sqlite:///tfr.db"
|
||||
"""
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import datetime
|
||||
import timeago
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
|
@ -11,3 +12,8 @@ def format_result(dttm):
|
|||
time = datetime.timedelta(seconds=int(dttm[0]))
|
||||
microtime = dttm[1][:3]
|
||||
return f"{time}:{microtime}"
|
||||
|
||||
|
||||
@blueprint.app_template_filter()
|
||||
def timesince(dttm):
|
||||
return timeago.format(dttm, datetime.datetime.now())
|
||||
|
|
Before Width: | Height: | Size: 570 KiB After Width: | Height: | Size: 256 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 11 KiB |
BIN
TFR/server/static/images/controls.png
Normal file
After Width: | Height: | Size: 360 KiB |
BIN
TFR/server/static/images/error.png
Normal file
After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 400 KiB |
Before Width: | Height: | Size: 165 KiB |
Before Width: | Height: | Size: 708 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 351 KiB |
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 40 KiB |
|
@ -12,6 +12,7 @@
|
|||
flex-direction: column
|
||||
|
||||
border: 1px solid RGBA($white, 0.1)
|
||||
box-sizing: content-box
|
||||
|
||||
> img
|
||||
height: 10rem
|
||||
|
@ -67,7 +68,11 @@
|
|||
gap: 0.5rem
|
||||
|
||||
> .text-input
|
||||
margin: 0 !important
|
||||
margin: 0
|
||||
|
||||
> button
|
||||
margin: 0 0 0 auto
|
||||
width: auto
|
||||
|
||||
@media (max-width: 621px)
|
||||
.profile-settings
|
||||
|
@ -80,3 +85,7 @@
|
|||
> img
|
||||
height: 13rem
|
||||
width: 13rem
|
||||
|
||||
.other > button
|
||||
margin: 0
|
||||
width: 100%
|
|
@ -90,9 +90,16 @@ body
|
|||
padding: 0.5rem
|
||||
text-align: center
|
||||
|
||||
&.player
|
||||
color: RGB($secondary)
|
||||
text-shadow: 0 0 5px RGBA($secondary, 0.7)
|
||||
> a
|
||||
color: RGB($white)
|
||||
text-decoration: none
|
||||
|
||||
&#you
|
||||
color: RGB($primary)
|
||||
text-shadow: 0 0 5px RGBA($primary, 0.4)
|
||||
|
||||
&:hover
|
||||
text-decoration: underline
|
||||
|
||||
> i
|
||||
padding: 0.25rem
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
<div class="center-text">
|
||||
<h2>{{ error }}</h2>
|
||||
<p>{{ msg }}</p>
|
||||
<image src="{{ url_for('static', filename='images/error/' + image + '.jpg') }}" alt="Error">
|
||||
<image src="{{ url_for('static', filename='images/error.png') }}" alt="Error">
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -3,6 +3,8 @@
|
|||
<h2>What is The Front Rooms?</h2>
|
||||
<p>The Front Rooms is a game based on The Backrooms Genre of games.</p>
|
||||
|
||||
<img src="{{ url_for('static', filename='images/controls.png') }}" alt="Drawing of keyboard displaying controls" width="500" height="500">
|
||||
|
||||
<h2>Is my data secured?</h2>
|
||||
<p>Yes, all passwords and emails are hashed and salted, and at no point stored in plain text.</p>
|
||||
{% endblock %}
|
|
@ -8,9 +8,9 @@
|
|||
<div class="profile-settings">
|
||||
<div class="picture">
|
||||
{% if current_user.picture %}
|
||||
<img src="{{ url_for('api.upload_dir', filename=current_user.picture) }}" alt="Profile picture">
|
||||
<img src="{{ url_for('api.upload_dir', filename=current_user.picture) }}" alt="Profile picture" id="picture-preview">
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='images/pfp.png') }}" alt="Profile picture">
|
||||
<img src="{{ url_for('static', filename='images/pfp.png') }}" alt="Profile picture" id="picture-preview">
|
||||
{% endif %}
|
||||
<label for="profile-picture">Profile Picture</label>
|
||||
<input type="file" name="file" id="profile-picture">
|
||||
|
@ -60,9 +60,26 @@
|
|||
</div>
|
||||
|
||||
|
||||
{% if not current_user.email %}
|
||||
<script>
|
||||
<script>
|
||||
{% if not current_user.email %}
|
||||
addFlashMessage("No Email set. If you loose your account, it will not be possible to recover it!", "error")
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
// Adjusted from https://stackoverflow.com/a/3814285/14885829
|
||||
document.getElementById('profile-picture').onchange = (event) => {
|
||||
let tgt = event.target || window.event.srcElement,
|
||||
files = tgt.files;
|
||||
|
||||
if (FileReader && files && files.length) {
|
||||
let fr = new FileReader();
|
||||
fr.onload = () => {
|
||||
document.getElementById('picture-preview').src = fr.result;
|
||||
}
|
||||
fr.readAsDataURL(files[0]);
|
||||
}
|
||||
else {
|
||||
addFlashMessage("Your browser could not show a preview of your profile picture!", "error")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -4,24 +4,23 @@
|
|||
<nav>
|
||||
<form method="GET" action="{{ url_for('views.index') }}" class="compact">
|
||||
<select name="diff" class="button">
|
||||
<option value="0" {% if diff==0 %}selected{% endif %}>Level 1</option>
|
||||
<option value="1" {% if diff==1 %}selected{% endif %}>Level 2</option>
|
||||
<option value="2" {% if diff==2 %}selected{% endif %}>Level 3</option>
|
||||
<option value="3" {% if diff==3 %}selected{% endif %}>Normal</option>
|
||||
<option value="4" {% if diff==4 %}selected{% endif %}>Hard</option>
|
||||
{% for difficulty in config["GAME_DIFFICULTIES"] %}
|
||||
<option value="{{ difficulty }}" {% if diff==difficulty %}selected{% endif %}>
|
||||
{{ config["GAME_DIFFICULTIES"][difficulty] }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<select name="ver" class="button">
|
||||
{% for game_version in config["GAME_VERSIONS"] %}
|
||||
<option
|
||||
value="{{ game_version }}"
|
||||
{% if ver==game_version %}selected{% endif %}
|
||||
>{{ game_version }}</option>
|
||||
{% for version in config["GAME_VERSIONS"] %}
|
||||
<option value="{{ version }}" {% if ver==version %}selected{% endif %}>
|
||||
{{ config["GAME_VERSIONS"][version] }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<span class="text-input">
|
||||
<label for="search">Username</label>
|
||||
<label for="search" style="min-width:auto">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
name="user"
|
||||
|
@ -45,11 +44,8 @@
|
|||
{% endif %}
|
||||
<div class="other">
|
||||
<h2>{{ user.username }}</h2>
|
||||
<p>Joined {{ user.joined_at.strftime('%Y-%m-%d') }}</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<p>Best score: {{ scores[0].score | format_result }}</p>
|
||||
<p>Joined {{ user.joined_at|timesince }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -58,7 +54,7 @@
|
|||
<div class="table">
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Pos</th>
|
||||
<th>Name</th>
|
||||
<th>Time Set</th>
|
||||
<th>Submitted</th>
|
||||
|
@ -74,14 +70,13 @@
|
|||
{% else %}
|
||||
<td>{{ loop.index }}</td>
|
||||
{% endif %}
|
||||
|
||||
{% if score.users.id == current_user.id %}
|
||||
<td class="player">{{ score.users.username }}</td>
|
||||
{% else %}
|
||||
<td>{{ score.users.username }}</td>
|
||||
{% endif %}
|
||||
|
||||
<td>{{ score.score | format_result }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('views.index', user=score.users.username) }}"
|
||||
{% if score.users.id == current_user.id %}id="you"{% endif %}>
|
||||
{{ score.users.username }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ score.score|format_result }}</td>
|
||||
<td>{{ score.scored_at.strftime('%Y-%m-%d') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -90,7 +85,7 @@
|
|||
{% else %}
|
||||
<div class="center-text">
|
||||
<h2>No scores</h2>
|
||||
<p>Go set some!</p>
|
||||
<p>We searched far and wide, but nothing was found</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|