mirror of
https://github.com/Fluffy-Bean/GameExpo23.git
synced 2025-05-19 09:44:52 +00:00
Updated Highscore server
This commit is contained in:
parent
3c3b6b8b23
commit
e7bbb02aba
26 changed files with 1023 additions and 321 deletions
|
@ -1,15 +1,16 @@
|
||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM python:3.10-alpine
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
RUN apk add --no-cache postgresql-client build-base
|
RUN apt update && apt install -y python3 python3-pip postgresql-client
|
||||||
WORKDIR /data
|
WORKDIR /data
|
||||||
|
|
||||||
COPY requirements.txt requirements.txt
|
COPY requirements.txt requirements.txt
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
RUN mkdir /storage
|
RUN mkdir /storage
|
||||||
COPY ./server .
|
COPY ./server ./server
|
||||||
|
COPY ./run.sh ./run.sh
|
||||||
RUN chmod +x ./run.sh
|
RUN chmod +x ./run.sh
|
||||||
|
|
||||||
CMD ["./run.sh"]
|
CMD ["./run.sh"]
|
||||||
|
|
|
@ -4,8 +4,8 @@ Flask-SQLAlchemy
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
Flask-Migrate
|
Flask-Migrate
|
||||||
Flask-Caching
|
Flask-Caching
|
||||||
Flask-wtf
|
|
||||||
Flask-Assets
|
Flask-Assets
|
||||||
|
Flask-Login
|
||||||
libsass
|
libsass
|
||||||
jsmin
|
jsmin
|
||||||
cssmin
|
cssmin
|
||||||
|
|
|
@ -17,7 +17,7 @@ then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if there are any changes to the database
|
# Check if there are any changes to the database
|
||||||
if $(flask --app server db check);
|
if ! $(flask --app server db check | grep -q "No changes in schema detected.");
|
||||||
then
|
then
|
||||||
echo "Database changes detected! Migrating..."
|
echo "Database changes detected! Migrating..."
|
||||||
flask --app server db migrate
|
flask --app server db migrate
|
50
Highscore-Server/server/__init__.py
Normal file
50
Highscore-Server/server/__init__.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
from random import randint
|
||||||
|
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 import views, auth, api
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_pyfile('config.py')
|
||||||
|
|
||||||
|
db.init_app(app)
|
||||||
|
migrate.init_app(app, db)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
96
Highscore-Server/server/api.py
Normal file
96
Highscore-Server/server/api.py
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
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
|
||||||
|
from server.config import BEARER_TOKEN
|
||||||
|
|
||||||
|
|
||||||
|
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=['POST'])
|
||||||
|
def post():
|
||||||
|
form = request.form
|
||||||
|
|
||||||
|
if not form:
|
||||||
|
return "Invalid form", 400
|
||||||
|
if not request.headers.get('Authentication'):
|
||||||
|
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 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_data := Tokens.query.filter_by(token=request.headers.get('Authentication')).first():
|
||||||
|
# User is authenticated
|
||||||
|
# This is a registered user
|
||||||
|
|
||||||
|
score = Scores(
|
||||||
|
score=form['score'],
|
||||||
|
difficulty=form['difficulty'],
|
||||||
|
achievements=form['achievements'],
|
||||||
|
user_id=token_data.holder,
|
||||||
|
)
|
||||||
|
db.session.add(score)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return "Success!", 200
|
||||||
|
elif request.headers.get('Authentication') == BEARER_TOKEN:
|
||||||
|
# User is not authenticated, but has the correct token
|
||||||
|
# This is an anonymous user
|
||||||
|
|
||||||
|
if not form['playerName'] or len(form['playerId']) != 4:
|
||||||
|
return "Invalid player name", 400
|
||||||
|
|
||||||
|
score = Scores(
|
||||||
|
anonymous=True,
|
||||||
|
username=form['playerName'],
|
||||||
|
score=form['score'],
|
||||||
|
difficulty=form['difficulty'],
|
||||||
|
)
|
||||||
|
db.session.add(score)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return "Success!", 200
|
||||||
|
|
||||||
|
return "Authentication failed", 401
|
101
Highscore-Server/server/auth.py
Normal file
101
Highscore-Server/server/auth.py
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
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"))
|
|
@ -2,8 +2,10 @@ from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from flask_assets import Environment
|
from flask_assets import Environment
|
||||||
from flask_caching import Cache
|
from flask_caching import Cache
|
||||||
|
from flask_login import LoginManager
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
migrate = Migrate()
|
migrate = Migrate()
|
||||||
assets = Environment()
|
assets = Environment()
|
||||||
cache = Cache(config={'CACHE_TYPE': 'simple'})
|
cache = Cache(config={'CACHE_TYPE': 'simple'})
|
||||||
|
login_manager = LoginManager()
|
||||||
|
|
|
@ -1,42 +1,69 @@
|
||||||
"""
|
"""
|
||||||
Database models for the server
|
Database models for the server
|
||||||
"""
|
"""
|
||||||
from extensions import db
|
import uuid
|
||||||
|
from flask_login import UserMixin
|
||||||
|
from server.extensions import db
|
||||||
|
|
||||||
|
|
||||||
class Scores(db.Model):
|
class Scores(db.Model):
|
||||||
"""
|
"""
|
||||||
Post table
|
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"
|
__tablename__ = "scores"
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
score = db.Column(db.Integer, nullable=False)
|
anonymous = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
difficulty = db.Column(db.String, nullable=False)
|
username = db.Column(db.String(32), nullable=True)
|
||||||
achievements = db.Column(db.String, nullable=False)
|
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
|
||||||
|
|
||||||
|
score = db.Column(db.Float, nullable=False)
|
||||||
|
difficulty = db.Column(db.Integer, nullable=False)
|
||||||
scored_at = db.Column(
|
scored_at = db.Column(
|
||||||
db.DateTime,
|
db.DateTime,
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default=db.func.now(),
|
server_default=db.func.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
scorer = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||||
|
|
||||||
class Users(db.Model):
|
|
||||||
|
class Users(db.Model, UserMixin):
|
||||||
"""
|
"""
|
||||||
User table
|
User table
|
||||||
"""
|
"""
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
steam_uuid = db.Column(db.String, unique=True, nullable=False)
|
alt_id = db.Column(db.String, nullable=False, unique=True)
|
||||||
steam_name = db.Column(db.String, nullable=False)
|
|
||||||
|
|
||||||
scores = db.relationship('Scores', backref='user', lazy=True)
|
username = db.Column(db.String(32), unique=True, nullable=False)
|
||||||
|
password = db.Column(db.String, nullable=False)
|
||||||
creation_data = db.Column(
|
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,
|
db.DateTime,
|
||||||
nullable=False,
|
nullable=False,
|
||||||
server_default=db.func.now(),
|
server_default=db.func.now(),
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
from flask import Flask
|
|
||||||
from flask_assets import Bundle
|
|
||||||
from extensions import db, migrate, cache, assets
|
|
||||||
from config import MIGRATION_DIR, INSTANCE_DIR
|
|
||||||
from views import blueprint
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
assets.init_app(app)
|
|
||||||
styles = Bundle("style.sass", filters="libsass, cssmin", output="gen/styles.css", depends="style.sass")
|
|
||||||
assets.register("styles", styles)
|
|
||||||
|
|
||||||
cache.init_app(app)
|
|
||||||
app.register_blueprint(blueprint)
|
|
BIN
Highscore-Server/server/static/error-images/1.jpg
Normal file
BIN
Highscore-Server/server/static/error-images/1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 400 KiB |
BIN
Highscore-Server/server/static/error-images/2.jpg
Normal file
BIN
Highscore-Server/server/static/error-images/2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 165 KiB |
BIN
Highscore-Server/server/static/error-images/3.jpg
Normal file
BIN
Highscore-Server/server/static/error-images/3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 708 KiB |
6
Highscore-Server/server/static/gen/scripts.js
Normal file
6
Highscore-Server/server/static/gen/scripts.js
Normal 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');}
|
1
Highscore-Server/server/static/gen/styles.css
Normal file
1
Highscore-Server/server/static/gen/styles.css
Normal file
File diff suppressed because one or more lines are too long
106
Highscore-Server/server/static/js/main.js
Normal file
106
Highscore-Server/server/static/js/main.js
Normal 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');
|
||||||
|
}
|
360
Highscore-Server/server/static/sass/style.sass
Normal file
360
Highscore-Server/server/static/sass/style.sass
Normal file
|
@ -0,0 +1,360 @@
|
||||||
|
$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
|
||||||
|
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
|
|
@ -1,111 +0,0 @@
|
||||||
$black: var(--black)
|
|
||||||
$white: var(--white)
|
|
||||||
$primary: var(--primary)
|
|
||||||
$secondary: var(--secondary)
|
|
||||||
$gold: var(--gold)
|
|
||||||
$silver: var(--silver)
|
|
||||||
$bronze: var(--bronze)
|
|
||||||
|
|
||||||
@import url('https://fonts.cdnfonts.com/css/cmu-serif')
|
|
||||||
|
|
||||||
\:root
|
|
||||||
--black: 21, 21, 21
|
|
||||||
--white: 232, 227, 227
|
|
||||||
--primary: 213, 214, 130
|
|
||||||
--secondary: 185, 77, 77
|
|
||||||
--gold: 255, 222, 70
|
|
||||||
--silver: 229, 220, 206
|
|
||||||
--bronze: 193, 145, 69
|
|
||||||
|
|
||||||
*
|
|
||||||
box-sizing: border-box
|
|
||||||
font-family: 'CMU Serif', serif
|
|
||||||
|
|
||||||
html
|
|
||||||
margin: 0
|
|
||||||
padding: 0
|
|
||||||
|
|
||||||
body
|
|
||||||
margin: 0
|
|
||||||
padding: 0
|
|
||||||
display: flex
|
|
||||||
flex-direction: row
|
|
||||||
background-color: RGB($black)
|
|
||||||
color: RGB($white)
|
|
||||||
|
|
||||||
.background
|
|
||||||
width: 100%
|
|
||||||
height: 100vh
|
|
||||||
height: 100dvh
|
|
||||||
|
|
||||||
object-fit: cover
|
|
||||||
position: absolute
|
|
||||||
z-index: 1
|
|
||||||
|
|
||||||
.app
|
|
||||||
margin: 0 auto
|
|
||||||
padding: 2rem
|
|
||||||
width: 800px
|
|
||||||
min-height: 100vh
|
|
||||||
background-color: RGBA($black, 0.9)
|
|
||||||
backdrop-filter: blur(0.5rem)
|
|
||||||
position: relative
|
|
||||||
z-index: 2
|
|
||||||
|
|
||||||
> table
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
.title
|
|
||||||
width: 100%
|
|
||||||
height: auto
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
.subtitle
|
|
||||||
margin-bottom: 1rem
|
|
||||||
padding: 0
|
|
||||||
|
|
||||||
text-align: center
|
|
||||||
font-weight: bolder
|
|
||||||
font-size: 1.2em
|
|
||||||
|
|
||||||
color: RGB($secondary)
|
|
||||||
|
|
||||||
> span
|
|
||||||
padding: 0 0.1rem
|
|
||||||
|
|
||||||
color: transparent
|
|
||||||
background: RGB($secondary)
|
|
||||||
|
|
||||||
nav
|
|
||||||
margin: 0
|
|
||||||
padding: 0
|
|
||||||
|
|
||||||
height: 3rem
|
|
||||||
|
|
||||||
display: flex
|
|
||||||
flex-direction: row
|
|
||||||
justify-content: center
|
|
||||||
|
|
||||||
> hr
|
|
||||||
margin: auto 0.25rem
|
|
||||||
|
|
||||||
height: 1.75rem
|
|
||||||
width: 3px
|
|
||||||
|
|
||||||
background-color: RGB($primary)
|
|
||||||
border: none
|
|
||||||
|
|
||||||
> a
|
|
||||||
margin: auto 0.25rem
|
|
||||||
padding: 0.5rem 1rem
|
|
||||||
|
|
||||||
text-decoration: none
|
|
||||||
|
|
||||||
background-color: transparent
|
|
||||||
color: RGB($primary)
|
|
||||||
border-radius: 2px
|
|
||||||
|
|
||||||
transition: background-color 0.2s ease-in-out
|
|
||||||
|
|
||||||
> a:hover
|
|
||||||
background-color: RGBA($primary, 0.3)
|
|
5
Highscore-Server/server/templates/about.html
Normal file
5
Highscore-Server/server/templates/about.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>What is The Front Rooms?</h2>
|
||||||
|
<h2>Project Redacted</h2>
|
||||||
|
{% endblock %}
|
32
Highscore-Server/server/templates/account.html
Normal file
32
Highscore-Server/server/templates/account.html
Normal 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 %}
|
22
Highscore-Server/server/templates/auth.html
Normal file
22
Highscore-Server/server/templates/auth.html
Normal 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 %}
|
|
@ -5,28 +5,62 @@
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Front Rooms Highscores</title>
|
<title>Front Rooms Highscores</title>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
||||||
|
|
||||||
|
{% assets "scripts" %}
|
||||||
|
<script src="{{ ASSET_URL }}"></script>
|
||||||
|
{% endassets %}
|
||||||
|
|
||||||
{% assets "styles" %}
|
{% assets "styles" %}
|
||||||
<link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css">
|
<link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css">
|
||||||
{% endassets %}
|
{% endassets %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<img src="{{ url_for("static", filename="bg.png") }}" alt="The Front Rooms pause menu" class="background">
|
<img src="{{ url_for('static', filename='bg.png') }}" alt="The Front Rooms pause menu" class="background">
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<img src="{{ url_for("static", filename="title.png") }}" alt="The Front Rooms logo" class="title">
|
<!-- Get flashed lol -->
|
||||||
<!-- <p class="subtitle">Project <span>Redacted</span></p> -->
|
<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="60px">
|
||||||
<nav>
|
<nav>
|
||||||
<a href="Level1">Level 1</a>
|
<a href="{{ url_for('views.index') }}" class="button">Scores</a>
|
||||||
<a href="Level2">Level 2</a>
|
<a href="{{ url_for('views.about') }}" class="button"><i class="ph ph-info"></i></a>
|
||||||
<a href="Level3">Level 3</a>
|
<a href="#" class="button"><i class="ph ph-download-simple"></i></a>
|
||||||
|
|
||||||
<hr>
|
<!-- This is a spacer -->
|
||||||
|
<span></span>
|
||||||
|
|
||||||
<a href="Normal">Normal</a>
|
{% if current_user.is_authenticated %}
|
||||||
<a href="Hard">Hard</a>
|
<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>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Secondary nav bar for page specific content -->
|
||||||
|
{% block nav %}{% endblock %}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>By Project Redacted | <a href="https://github.com/Fluffy-Bean/GameExpo23">Server Source</a></p>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
8
Highscore-Server/server/templates/error.html
Normal file
8
Highscore-Server/server/templates/error.html
Normal 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 %}
|
|
@ -1,20 +1,42 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% 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>
|
||||||
|
|
||||||
<table>
|
<!-- This is a spacer -->
|
||||||
|
<span></span>
|
||||||
|
</nav>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
{% if scores %}
|
||||||
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Score</th>
|
<th>Position</th>
|
||||||
<th>Difficulty</th>
|
|
||||||
<th>Achievements</th>
|
|
||||||
<th>Player</th>
|
<th>Player</th>
|
||||||
|
<th>Difficulty</th>
|
||||||
|
<th>Score</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for score in top_scores %}
|
{% for score in top_scores %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ score.score }}</td>
|
<td>{{ loop.index }}</td>
|
||||||
|
{% if score.anonymous %}
|
||||||
|
<td>{{ score.username }}</td>
|
||||||
|
{% else %}
|
||||||
|
<td>{{ score.scorer.username }}</td>
|
||||||
|
{% endif %}
|
||||||
<td>{{ score.difficulty }}</td>
|
<td>{{ score.difficulty }}</td>
|
||||||
<td>{{ score.achievements }}</td>
|
<td>{{ score.score }}</td>
|
||||||
<td>{{ score.user.steam_name }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="center-text">
|
||||||
|
<h2>No scores yet</h2>
|
||||||
|
<p>Set some!</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -1,62 +1,23 @@
|
||||||
from flask import Blueprint, jsonify, request, render_template
|
from flask import Blueprint, request, render_template
|
||||||
from flask_wtf import FlaskForm
|
from server.models import Scores
|
||||||
from wtforms import StringField, IntegerField
|
|
||||||
from wtforms.validators import DataRequired
|
|
||||||
|
|
||||||
from models import Scores, Users
|
|
||||||
from extensions import db, cache
|
|
||||||
from config import BEARER_TOKEN
|
|
||||||
|
|
||||||
|
|
||||||
blueprint = Blueprint('views', __name__)
|
blueprint = Blueprint('views', __name__)
|
||||||
|
|
||||||
|
|
||||||
class ScoreForm(FlaskForm):
|
@blueprint.route('/')
|
||||||
playerName = StringField('Player Name', validators=[DataRequired()])
|
# @cache.cached(timeout=60)
|
||||||
playerId = StringField('Player ID', validators=[DataRequired()])
|
|
||||||
score = IntegerField('Score', validators=[DataRequired()])
|
|
||||||
difficulty = StringField('Difficulty', validators=[DataRequired()])
|
|
||||||
achievements = StringField('Achievements', validators=[DataRequired()])
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/', methods=['GET'])
|
|
||||||
@cache.cached(timeout=60)
|
|
||||||
def index():
|
def index():
|
||||||
top_scores = Scores.query.order_by(Scores.score.desc()).limit(10).all()
|
difficulty = request.args.get('diff', 0)
|
||||||
|
|
||||||
|
top_scores = (Scores.query
|
||||||
|
.order_by(Scores.score.desc())
|
||||||
|
.filter_by(difficulty=difficulty)
|
||||||
|
.limit(10)
|
||||||
|
.all())
|
||||||
return render_template('scores.html', top_scores=top_scores)
|
return render_template('scores.html', top_scores=top_scores)
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/post', methods=['POST'])
|
@blueprint.route('/about')
|
||||||
def post():
|
def about():
|
||||||
form = ScoreForm()
|
return render_template('about.html')
|
||||||
|
|
||||||
if not form:
|
|
||||||
return "Invalid form", 400
|
|
||||||
if request.headers.get('Authentication') != 'Bearer ' + BEARER_TOKEN:
|
|
||||||
return "Invalid authentication", 401
|
|
||||||
|
|
||||||
if not isinstance(form.score.data, int):
|
|
||||||
return "Score must be an integer", 400
|
|
||||||
if form.score.data < 0:
|
|
||||||
return "Score must be greater than 0", 400
|
|
||||||
if form.difficulty.data not in ['easy', 'medium', 'hard']:
|
|
||||||
return "Invalid difficulty", 400
|
|
||||||
|
|
||||||
user = Users.query.filter_by(steam_uuid=form.playerId.data).first()
|
|
||||||
if not user:
|
|
||||||
user = Users(
|
|
||||||
steam_uuid=form.playerId.data,
|
|
||||||
steam_name=form.playerName.data,
|
|
||||||
)
|
|
||||||
db.session.add(user)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
score = Scores(
|
|
||||||
score=form.score.data,
|
|
||||||
difficulty=form.difficulty.data,
|
|
||||||
achievements=form.achievements.data,
|
|
||||||
user_id=user.id,
|
|
||||||
)
|
|
||||||
db.session.add(score)
|
|
||||||
db.session.commit()
|
|
||||||
return jsonify({'message': 'Success!'})
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ services:
|
||||||
image: caddy:alpine
|
image: caddy:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
# - 80:80
|
||||||
- 443:443
|
- 443:443
|
||||||
volumes:
|
volumes:
|
||||||
- ./Caddy/Caddyfile:/etc/caddy/Caddyfile
|
- ./Caddy/Caddyfile:/etc/caddy/Caddyfile
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue