Fuck so much to comment on

Renamed the folders and containers to something more reasonable
Using .env file for secretes so I can better hide them from git
Mostly it, I think
This commit is contained in:
Michał Gdula 2023-06-09 22:27:30 +03:00
parent a29f06dfee
commit a4ebfa8552
61 changed files with 84 additions and 111 deletions

163
TFR/.gitignore vendored Normal file
View file

@ -0,0 +1,163 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
# remove development files
/storage
/logs

17
TFR/Dockerfile Normal file
View file

@ -0,0 +1,17 @@
# syntax=docker/dockerfile:1
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
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
RUN mkdir /storage
COPY ./server ./server
COPY ./run.sh ./run.sh
RUN chmod +x ./run.sh
CMD ["./run.sh"]

2
TFR/README.md Normal file
View file

@ -0,0 +1,2 @@
# Highscores-Server
Server (and website) to store and display Highscores of the players who completed the game

11
TFR/requirements.txt Normal file
View file

@ -0,0 +1,11 @@
Gunicorn
psycopg2-binary
Flask
Flask-SQLAlchemy
Flask-Migrate
Flask-Login
Flask-Assets
Flask-Caching
libsass-bin
jsmin
cssmin

29
TFR/run.sh Normal file
View file

@ -0,0 +1,29 @@
#!/bin/sh
# Wait for database to start
until pg_isready -d $DB_NAME -h $DB_HOST -U $DB_USER
do
echo "Waiting for database to start... (5s)"
sleep 5
done
echo "Database is ready!"
# Check if migrastions folder exists
if [ ! -d "/data/storage/migrations" ];
then
echo "Creating tables..."
flask --app server db init
fi
# Check if there are any changes to the database
if ! $(flask --app server db check | grep -q "No changes in schema detected.");
then
echo "Database changes detected! Migrating..."
flask --app server db migrate
flask --app server db upgrade
fi
# Start server!!!!
echo "Starting server..."
gunicorn --bind tfr:8000 server:app

58
TFR/server/__init__.py Normal file
View file

@ -0,0 +1,58 @@
from random import randint
from flask import Flask, render_template, abort
from flask_assets import Bundle
from werkzeug.exceptions import HTTPException
from server.config import MIGRATION_DIR, INSTANCE_DIR
from server.extensions import db, migrate, cache, assets, login_manager
from server.models import Users
from server import views, auth, api
app = Flask(__name__, instance_path=INSTANCE_DIR)
app.config.from_pyfile("config.py")
db.init_app(app)
migrate.init_app(app, db, directory=MIGRATION_DIR)
with app.app_context():
db.create_all()
login_manager.init_app(app)
login_manager.login_view = "auth.auth"
assets.init_app(app)
scripts = Bundle("js/*.js", filters="jsmin", output="gen/scripts.js", depends="js/*.js")
assets.register("scripts", scripts)
styles = Bundle(
"sass/style.sass",
filters="libsass, cssmin",
output="gen/styles.css",
depends="sass/*.sass",
)
assets.register("styles", styles)
cache.init_app(app)
app.register_blueprint(views.blueprint)
app.register_blueprint(auth.blueprint)
app.register_blueprint(api.blueprint)
@login_manager.user_loader
def load_user(user_id):
return Users.query.filter_by(alt_id=user_id).first()
@app.errorhandler(Exception)
def error_page(err):
if not isinstance(err, HTTPException):
abort(500)
return (
render_template(
"error.html", error=err.code, msg=err.description, image=str(randint(1, 3))
),
err.code,
)

86
TFR/server/api.py Normal file
View file

@ -0,0 +1,86 @@
import uuid
from flask import Blueprint, request, jsonify
from flask_login import login_required, current_user
from server.models import Tokens, Scores
from server.extensions import db
blueprint = Blueprint("api", __name__, url_prefix="/api")
@blueprint.route("/tokens", methods=["DELETE", "POST"])
@login_required
def tokens():
if request.method == "DELETE":
token_id = request.form["token_id"]
if not token_id:
return jsonify({"error": "No token ID provided!"}), 400
token = Tokens.query.filter_by(id=token_id).first()
if not token:
return jsonify({"error": "Token not found!"}), 404
if token.holder != current_user.id:
return jsonify({"error": "You do not own this token!"}), 403
db.session.delete(token)
db.session.commit()
return jsonify({"success": "Token deleted!"}), 200
elif request.method == "POST":
if len(Tokens.query.filter_by(holder=current_user.id).all()) >= 5:
return jsonify({"error": "You already have 5 tokens!"}), 403
token = Tokens(token=str(uuid.uuid4()), holder=current_user.id)
db.session.add(token)
db.session.commit()
return jsonify({"success": "Token added!"}), 200
@blueprint.route("/post", methods=["GET", "POST"])
def post():
if request.method == "GET":
return """
<form method="POST">
<input name="score">
<input name="difficulty">
<input name="token">
<button type="submit">Sub</button>
</form>
"""
form = request.form
if not form:
return "Invalid form", 400
if not form["token"]:
return "Invalid authentication", 401
# if not isinstance(form["score"], int):
# return "Score must be an integer", 400
if int(form["score"]) < 0:
return "Score must be greater than 0", 400
if int(form["difficulty"]) not in [0, 1, 2, 3, 4]:
# 0 = Easy, Level 1
# 1 = Easy, Level 2
# 2 = Easy, Level 3
# 3 = Normal
# 4 = Hard
return "Invalid difficulty", 400
if token := Tokens.query.filter_by(token=form["token"]).first():
# Yupeee, authenticated
score = Scores(
score=int(form["score"]),
difficulty=int(form["difficulty"]),
scorer=token.holder,
)
db.session.add(score)
db.session.commit()
return "Success!", 200
# L no authentication :3
return "Authentication failed", 401

103
TFR/server/auth.py Normal file
View file

@ -0,0 +1,103 @@
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 werkzeug.security import generate_password_hash, check_password_hash
from server.extensions import db
from server.models import Users, Tokens
blueprint = Blueprint("auth", __name__)
@blueprint.route("/auth", methods=["GET"])
def auth():
return render_template("auth.html")
@blueprint.route("/account", methods=["GET"])
@login_required
def account():
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")
token_list = Tokens.query.filter_by(holder=current_user.id).all()
return render_template("account.html", token_list=token_list)
@blueprint.route("/register", methods=["POST"])
def register():
# Get the form data
username = request.form["username"].strip()
password = request.form["password"].strip()
username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b")
error = []
# Validate the form
if not username or not username_regex.match(username):
error.append(
"Username is empty or invalid! Must be alphanumeric, and can contain ._-"
)
if not password:
error.append("Password is empty!")
elif len(password) < 8:
error.append("Password is too short! Must be at least 8 characters long.")
if Users.query.filter_by(username=username).first():
error.append("Username already exists!")
# If there are errors, return them
if error:
for err in error:
flash(err, "error")
return redirect(url_for("auth.auth"))
register_user = Users(
alt_id=str(uuid.uuid4()),
username=username,
password=generate_password_hash(password, method="scrypt"),
)
db.session.add(register_user)
db.session.commit()
flash("Successfully registered!", "success")
return redirect(url_for("auth.auth"))
@blueprint.route("/login", methods=["POST"])
def login():
# Get the form data
username = request.form["username"].strip()
password = request.form["password"].strip()
username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b")
error = []
# Validate the form
if not username or not username_regex.match(username) or not password:
error.append("Username or Password is incorrect!")
user = Users.query.filter_by(username=username).first()
if not user or not check_password_hash(user.password, password):
error.append("Username or Password is incorrect!")
# If there are errors, return them
if error:
for err in error:
flash(err, "error")
return redirect(url_for("auth.account"))
login_user(user, remember=True)
flash("Successfully logged in!", "success")
return redirect(url_for("views.index"))

15
TFR/server/config.py Normal file
View file

@ -0,0 +1,15 @@
import os
SECRET_KEY = os.getenv("FLASK_KEY")
user = os.getenv("DB_USER")
password = os.getenv("DB_PASSWORD")
host = os.getenv("DB_HOST")
db = os.getenv("DB_NAME")
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{user}:{password}@{host}:5432/{db}"
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_POOL_RECYCLE = 621
MIGRATION_DIR = "/data/storage/migrations"
INSTANCE_DIR = "/data/storage/instance"

11
TFR/server/extensions.py Normal file
View file

@ -0,0 +1,11 @@
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
db = SQLAlchemy()
migrate = Migrate()
assets = Environment()
cache = Cache(config={"CACHE_TYPE": "simple"})
login_manager = LoginManager()

70
TFR/server/models.py Normal file
View file

@ -0,0 +1,70 @@
"""
Database models for the server
"""
import uuid
from flask_login import UserMixin
from server.extensions import db
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
"""
__tablename__ = "scores"
id = db.Column(db.Integer, primary_key=True)
score = db.Column(db.Float, nullable=False)
difficulty = db.Column(db.Integer, nullable=False)
scored_at = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.now(),
)
scorer = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
class Users(db.Model, UserMixin):
"""
User table
"""
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
alt_id = db.Column(db.String, nullable=False, unique=True)
username = db.Column(db.String(32), unique=True, nullable=False)
password = db.Column(db.String, nullable=False)
joined_at = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.now(),
)
scores = db.relationship("Scores", backref="user", lazy=True)
tokens = db.relationship("Tokens", backref="user", lazy=True)
def get_id(self):
return str(self.alt_id)
class Tokens(db.Model):
"""
Token table
"""
__tablename__ = "tokens"
id = db.Column(db.Integer, primary_key=True)
holder = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
token = db.Column(db.String, nullable=False, unique=True)
created_at = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.now(),
)

BIN
TFR/server/static/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 KiB

View file

@ -0,0 +1,6 @@
function addFlashMessage(message,type='success'){let flask=document.createElement('p');flask.onclick=()=>flask.remove();flask.classList.add(type);flask.innerHTML=message;let close=document.createElement('span');close.innerHTML='<i class="ph-bold ph-x"></i>';flask.appendChild(close);document.querySelector('.flash').appendChild(flask);}
function ajax(url,form,callback,method='POST'){console.log(form)
fetch(url,{method:method,body:form,}).then(response=>response.json()).then(data=>callback(data)).catch(error=>addFlashMessage(error.error,'error'));}
function deleteToken(id){let form=new FormData();form.append('token_id',id);ajax('/api/tokens',form,(data)=>{if(data.success){addFlashMessage(data.success,'success');document.querySelector(`#token-${id}`).remove();}else{addFlashMessage(data.error,'error');}},'DELETE');}
function addToken(){ajax('/api/tokens',null,(data)=>{if(data.success){window.location.reload();}else{addFlashMessage(data.error,'error');}});}
function viewToken(id){let token=document.querySelector(`#token-${id}`);let hidden=token.children[2];hidden.classList.toggle('hidden');}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,106 @@
function addFlashMessage(message, type='success') {
/**
* Add a flash message to the page
*
* @param {string} message
* @return {void}
*
* @example
* addFlashMessage('Hello World!', 'success')
*
* @example
* addFlashMessage('Oopsie!', 'error')
*/
let flask = document.createElement('p');
flask.onclick = () => flask.remove();
flask.classList.add(type);
flask.innerHTML = message;
let close = document.createElement('span');
close.innerHTML = '<i class="ph-bold ph-x"></i>';
flask.appendChild(close);
document.querySelector('.flash').appendChild(flask);
}
function ajax(url, form, callback, method='POST') {
/**
* Send a request to the server and get a response
* Mostly a wrapper for fetch(), since most of the
* requests are made with FormData and POST method
*
* @param {string} url
* @param {FormData} form
* @param {function} callback
* @param {string} method
* @return {void}
*
* @example
* ajax('/api', formData, callback = (data) => { console.log(data) }, 'POST')
*/
console.log(form)
fetch(url, {
method: method,
body: form,
})
.then(response => response.json())
.then(data => callback(data))
.catch(error => addFlashMessage(error.error, 'error'));
}
function deleteToken(id) {
/**
* Delete user token
*
* @return {void}
* @{integer} id
*
* @example
* deleteToken(id)
*/
let form = new FormData();
form.append('token_id', id);
ajax('/api/tokens', form, (data) => {
if (data.success) {
addFlashMessage(data.success, 'success');
document.querySelector(`#token-${id}`).remove();
} else {
addFlashMessage(data.error, 'error');
}
}, 'DELETE');
}
function addToken() {
/**
* Add a new token
*
* @return {void}
*
* @example
* addToken()
*/
ajax('/api/tokens', null, (data) => {
if (data.success) {
window.location.reload();
} else {
addFlashMessage(data.error, 'error');
}
});
}
function viewToken(id) {
/**
* View a token
*
* @return {void}
* @{integer} id
*
* @example
* viewToken(id)
*/
let token = document.querySelector(`#token-${id}`);
let hidden = token.children[2];
hidden.classList.toggle('hidden');
}

View file

@ -0,0 +1,362 @@
$black: var(--black)
$white: var(--white)
$primary: var(--primary)
$secondary: var(--secondary)
$gold: var(--gold)
$silver: var(--silver)
$bronze: var(--bronze)
$darkBlue: var(--darkBlue)
@mixin button($color)
text-decoration: none
text-align: center
white-space: nowrap
background-color: RGBA($color, 0.02)
color: RGB($color)
border-radius: 2px
border: 0 solid transparent
transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out
&:hover
background-color: RGBA($color, 0.3)
transform: translateY(-0.1rem)
@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')
\:root
--black: 21, 21, 21
--white: 232, 227, 227
--primary: 210, 206, 97
--secondary: 185, 77, 77
--gold: 255, 222, 70
--silver: 229, 220, 206
--bronze: 193, 145, 69
--darkBlue: 9, 9, 39
*
box-sizing: border-box
font-family: 'Merriweather', serif
html
margin: 0
padding: 0
body
margin: 0
padding: 0
display: flex
flex-direction: row
background-color: RGB($darkBlue)
color: RGB($white)
.background
width: 100%
height: 100%
object-fit: cover
position: absolute
z-index: 1
.app
margin: 0 auto
padding: 0
width: 800px
min-height: 100vh
position: relative
display: flex
flex-direction: column
background-color: rgba($darkBlue, 0.7)
backdrop-filter: blur(5px)
z-index: 2
> table
width: 100%
header
padding: 1rem
background-color: RGBA($darkBlue, 0.7)
> img
margin-bottom: 1rem
width: 100%
height: auto
text-align: center
nav
margin-top: 0.3rem
padding: 0
display: flex
flex-direction: row
justify-content: center
> span
width: 100%
> a
margin: auto 0.15rem
padding: 0.5rem 0.7rem
text-decoration: none
white-space: nowrap
font-size: 0.9em
color: RGB($primary)
&.button
@include button($white)
&.primary
@include button($primary)
&.secondary
@include button($secondary)
> i
font-size: 1.25em
display: block
.flash
display: flex
flex-direction: column
justify-content: center
align-items: center
> p
margin: 0
padding: 0.75rem 1rem
width: 100%
position: relative
background-color: RGB($darkBlue)
color: RGB($primary)
transition: background-color 0.2s ease-in-out, padding 0.2s ease-in-out
> span
position: absolute
top: 0
left: 0
bottom: 0
width: 0.25rem
display: flex
justify-content: center
align-items: center
background-color: RGB($primary)
color: transparent
overflow: hidden
transition: width 0.2s ease-in-out, color 0.2s ease-in-out, background-color 0.2s ease-in-out
> i
font-size: 1.25em
&:hover
padding: 0.75rem 1rem 0.75rem 4rem
background-color: RGBA($primary, 0.1)
cursor: pointer
> span
width: 3rem
color: RGB($black)
&.success
color: RGB($primary)
> span
background-color: RGB($primary)
&:hover
background-color: RGBA($primary, 0.1)
&.error
color: RGB($secondary)
> span
background-color: RGB($secondary)
&:hover
background-color: RGBA($secondary, 0.1)
main
padding: 1rem
height: 100%
display: flex
flex-direction: column
> h2
margin: 0 0 1rem 0
font-size: 1.5em
color: RGB($white)
.center-text
height: 100%
display: flex
flex-direction: column
justify-content: center
align-items: center
> h2
margin: 0
text-align: center
font-size: 2em
color: RGB($white)
> p
margin: 0
text-align: center
font-size: 1em
color: RGB($white)
> img
margin: 1rem auto 0
max-width: 100%
max-height: 15rem
border-radius: 2px
.block
margin-bottom: 1rem
padding: 1rem
display: flex
flex-direction: column
background-color: rgba($darkBlue, 0.7)
border-radius: 2px
> h2
margin: 0 0 0.2rem 0
font-size: 1.3em
color: RGB($white)
> p
margin: 0 0 1rem 0
font-size: 1em
.button
margin: 0
padding: 0.5rem 0.7rem
text-decoration: none
white-space: nowrap
font-size: 0.9em
color: RGB($primary)
> i
font-size: 1.25em
display: block
@include button($white)
&.primary
@include button($primary)
&.secondary
@include button($secondary)
> table
width: 100%
border-collapse: collapse
> tbody > tr > td
padding: 0 0.5rem 0.5rem 0
text-align: left
font-size: 0.9em
color: RGB($white)
transition: filter 0.2s ease-in-out
&:last-child
width: 100%
&.hidden
filter: blur(5px)
&.secondary
border: 1px solid RGB($secondary)
> h2
color: RGB($secondary)
form
display: flex
flex-direction: column
> input
margin: 0 0 1rem 0
padding: 0.7rem 1rem
border: 1px solid RGB($white)
border-radius: 2px
background-color: RGB($darkBlue)
color: RGB($white)
&:focus
outline: none
border-color: RGB($primary)
&.error
border-color: RGB($secondary)
> button
margin: 0
padding: 0.75rem 1rem
font-weight: bolder
border: transparent
border-radius: 2px
background-color: RGB($primary)
color: RGB($black)
&:focus-visible, &:hover
outline: none
background-color: RGBA($primary, 0.3)
color: RGB($primary)
&.disabled
pointer-events: none
opacity: 0.5
&.secondary
background-color: RGB($secondary)
color: RGB($black)
&:focus-visible, &:hover
background-color: RGBA($secondary, 0.3)
color: RGB($secondary)
footer
padding: 0.5rem 1rem
width: 100%
display: flex
flex-direction: row
background-color: RGBA($darkBlue, 0.7)
> p
margin: 0
width: 100%
text-align: center
font-size: 0.8em
white-space: nowrap
color: RGB($white)
> a
color: RGB($secondary)
text-decoration: none
&:hover
text-decoration: underline

BIN
TFR/server/static/title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

View file

@ -0,0 +1,5 @@
{% extends "base.html" %}
{% block content %}
<h2>What is The Front Rooms?</h2>
<h2>Project Redacted</h2>
{% endblock %}

View file

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block nav %}
<nav>
<a href="{{ url_for('auth.account', action='logout') }}" class="button">Your Profile</a>
<!-- This is a spacer -->
<span></span>
<a href="{{ url_for('auth.account', action='logout') }}" class="button secondary">Logout</a>
</nav>
{% endblock %}
{% block content %}
<div class="block">
<h2>Tokens</h2>
<p>These are your API tokens. Used to link the uploaded scores with your account.</p>
<table>
{% for token in token_list %}
<tr id="token-{{ token.id }}">
<td><button onclick="deleteToken({{ token.id }})" class="button secondary"><i class="ph ph-trash"></i></button></td>
<td><button onclick="viewToken({{ token.id }})" class="button primary"><i class="ph ph-eye"></i></button></td>
<td class="hidden">{{ token.token }}</td>
</tr>
{% endfor %}
</table>
<button onclick="addToken()" class="button primary">Create New Token</button>
</div>
<div class="block secondary">
<h2>Danger Zone</h2>
<p>These actions are irreversible. Be careful!</p>
<a href="{{ url_for('auth.account', action='delete') }}" class="button secondary">Delete Account</a>
<a href="{{ url_for('auth.account', action='password') }}" class="button secondary">Reset Password</a>
</div>
{% endblock %}

View file

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block content %}
<div class="block">
<h2>Login</h2>
<p>Welcome back!</p>
<form action="{{ url_for('auth.login') }}" method="POST">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
</div>
<div class="block">
<h2>Register</h2>
<p>Don't have an account? Register here!</p>
<form action="{{ url_for('auth.register') }}" method="POST">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Register</button>
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Front Rooms Highscores</title>
<script src="https://unpkg.com/@phosphor-icons/web"></script>
{% assets "scripts" %}
<script src="{{ ASSET_URL }}"></script>
{% endassets %}
{% assets "styles" %}
<link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css">
{% endassets %}
</head>
<body>
<img src="{{ url_for('static', filename='bg.png') }}" alt="The Front Rooms pause menu" class="background">
<div class="app">
<!-- Get flashed lol -->
<div class="flash">
{% 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>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<header>
<img src="{{ url_for('static', filename='title.png') }}" alt="The Front Rooms logo" class="title" height="82px">
<nav>
<a href="{{ url_for('views.index') }}" class="button">Scores</a>
<a href="{{ url_for('views.about') }}" class="button"><i class="ph ph-info"></i></a>
<a href="#" class="button"><i class="ph ph-download-simple"></i></a>
<!-- This is a spacer -->
<span></span>
{% if current_user.is_authenticated %}
<a href="{{ url_for('auth.account') }}" class="button primary">{{ current_user.username }}</a>
{% else %}
<a href="{{ url_for('auth.auth') }}" class="button primary"><i class="ph ph-identification-card"></i></a>
{% endif %}
</nav>
<!-- Secondary nav bar for page specific content -->
{% block nav %}{% endblock %}
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>By Project Redacted | <a href="https://github.com/Fluffy-Bean/GameExpo23">Server Source</a></p>
</footer>
</div>
</body>
</html>

View file

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<div class="center-text">
<h2>{{ error }}</h2>
<p>{{ msg }}</p>
<image src="{{ url_for('static', filename='error-images/' + image + '.jpg') }}" alt="Error">
</div>
{% endblock %}

View file

@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block nav %}
<nav>
<a href="{{ url_for('views.index', diff=0) }}" class="button">Level 1</a>
<a href="{{ url_for('views.index', diff=1) }}" class="button">Level 2</a>
<a href="{{ url_for('views.index', diff=2) }}" class="button">Level 3</a>
<a href="{{ url_for('views.index', diff=3) }}" class="button">Normal</a>
<a href="{{ url_for('views.index', diff=4) }}" class="button">Hard</a>
<!-- This is a spacer -->
<span></span>
</nav>
{% endblock %}
{% block content %}
{% if scores %}
<table>
<tr>
<th>Position</th>
<th>Player</th>
<th>Difficulty</th>
<th>Score</th>
</tr>
{% for score in scores %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ score.scorer.username }}</td>
<td>{{ score.difficulty }}</td>
<td>{{ score.score }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="center-text">
<h2>No scores yet</h2>
<p>Set some!</p>
</div>
{% endif %}
{% endblock %}

24
TFR/server/views.py Normal file
View file

@ -0,0 +1,24 @@
from flask import Blueprint, request, render_template
from server.models import Scores
blueprint = Blueprint("views", __name__)
@blueprint.route("/")
# @cache.cached(timeout=60)
def index():
difficulty = request.args.get("diff", 0)
scores = (
Scores.query.filter_by(difficulty=difficulty)
.order_by(Scores.score.desc())
.limit(10)
.all()
)
return render_template("scores.html", scores=scores)
@blueprint.route("/about")
def about():
return render_template("about.html")