mirror of
https://github.com/Project-Redacted/Highscores-Server.git
synced 2025-05-14 07:32:15 +00:00
Sassy
This commit is contained in:
parent
62945c943d
commit
ebdef07840
7 changed files with 222 additions and 127 deletions
|
@ -1,6 +1,7 @@
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_assets import Bundle
|
from flask_assets import Bundle
|
||||||
from server.extensions import db, migrate, cache, assets
|
from server.extensions import db, migrate, cache, assets, login_manager
|
||||||
|
from server.models import Users
|
||||||
from server import views, auth
|
from server import views, auth
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
@ -12,6 +13,9 @@ migrate.init_app(app, db)
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
||||||
|
login_manager.init_app(app)
|
||||||
|
login_manager.login_view = "auth.auth"
|
||||||
|
|
||||||
assets.init_app(app)
|
assets.init_app(app)
|
||||||
styles = Bundle("style.sass", filters="libsass, cssmin", output="gen/styles.css", depends="style.sass")
|
styles = Bundle("style.sass", filters="libsass, cssmin", output="gen/styles.css", depends="style.sass")
|
||||||
assets.register("styles", styles)
|
assets.register("styles", styles)
|
||||||
|
@ -19,3 +23,8 @@ assets.register("styles", styles)
|
||||||
cache.init_app(app)
|
cache.init_app(app)
|
||||||
app.register_blueprint(views.blueprint)
|
app.register_blueprint(views.blueprint)
|
||||||
app.register_blueprint(auth.blueprint)
|
app.register_blueprint(auth.blueprint)
|
||||||
|
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(user_id):
|
||||||
|
return Users.query.filter_by(alt_id=user_id).first()
|
||||||
|
|
|
@ -1,25 +1,56 @@
|
||||||
from flask import Blueprint, jsonify, request, render_template
|
import re
|
||||||
from flask_wtf import FlaskForm
|
|
||||||
from wtforms import StringField, IntegerField
|
|
||||||
from wtforms.validators import DataRequired
|
|
||||||
|
|
||||||
from server.models import Scores, Users, Tokens
|
from flask import Blueprint, render_template, request, flash, redirect, url_for
|
||||||
from server.extensions import db, cache
|
from flask_login import current_user, login_required
|
||||||
from server.config import BEARER_TOKEN
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
|
from server.extensions import cache, login_manager, db
|
||||||
|
from server.models import Users
|
||||||
|
|
||||||
|
|
||||||
blueprint = Blueprint('auth', __name__)
|
blueprint = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
|
||||||
class ScoreForm(FlaskForm):
|
|
||||||
playerName = StringField('Player Name', validators=[DataRequired()])
|
|
||||||
playerId = StringField('Player ID', validators=[DataRequired()])
|
|
||||||
score = IntegerField('Score', validators=[DataRequired()])
|
|
||||||
difficulty = StringField('Difficulty', validators=[DataRequired()])
|
|
||||||
achievements = StringField('Achievements', validators=[DataRequired()])
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/auth', methods=['GET'])
|
@blueprint.route('/auth', methods=['GET'])
|
||||||
@cache.cached(timeout=60)
|
|
||||||
def auth():
|
def auth():
|
||||||
return render_template('auth.html')
|
return render_template('auth.html')
|
||||||
|
|
||||||
|
@blueprint.route('/account', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def account():
|
||||||
|
return render_template('account.html')
|
||||||
|
|
||||||
|
|
||||||
|
@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(
|
||||||
|
username=username,
|
||||||
|
password=generate_password_hash(password, method="sha256"),
|
||||||
|
)
|
||||||
|
db.session.add(register_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return redirect(url_for("view.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 +1 @@
|
||||||
@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:213,214,130;--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(var(--darkBlue));color:RGB(var(--white))}.background{width:100%;height:100%;object-fit:cover;position:absolute;z-index:1}.app{margin:0 auto;padding:2rem;width:800px;min-height:100vh;position:relative;display:flex;flex-direction:column;background-color:rgba(var(--darkBlue),0.9);z-index:2}.app>table{width:100%}.app .center-text{height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center}.app .center-text>h2{margin:0;text-align:center;font-size:2em;color:RGB(var(--white))}.app .center-text>p{margin:0;text-align:center;font-size:1em;color:RGB(var(--white))}.app .auth{margin:auto 1rem;padding:1rem;display:flex;flex-direction:column;background-color:rgba(var(--darkBlue),0.5);border-radius:2px}.app .auth>h2{margin:0 0 1rem 0;font-size:1.3em;color:RGB(var(--white))}.title{margin-bottom:2rem;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(var(--secondary))}.subtitle>span{padding:0 .1rem;color:transparent;background:RGB(var(--secondary))}nav{margin:0;padding:0;height:3rem;display:flex;flex-direction:row;justify-content:center}nav>span{width:100%}nav>a{margin:auto .25rem;padding:.5rem 1rem;text-decoration:none;white-space:nowrap;color:RGB(var(--primary))}nav>a.button{text-decoration:none;background-color:transparent;color:RGB(var(--white));border-radius:2px;transition:background-color .2s ease-in-out,box-shadow .2s ease-in-out,transform .2s ease-in-out}nav>a.button:hover{background-color:RGBA(var(--white),0.3);transform:translateY(-0.1rem)}nav>a.button.primary{text-decoration:none;background-color:transparent;color:RGB(var(--primary));border-radius:2px;transition:background-color .2s ease-in-out,box-shadow .2s ease-in-out,transform .2s ease-in-out}nav>a.button.primary:hover{background-color:RGBA(var(--primary),0.3);transform:translateY(-0.1rem)}nav>a.button.secondary{text-decoration:none;background-color:transparent;color:RGB(var(--secondary));border-radius:2px;transition:background-color .2s ease-in-out,box-shadow .2s ease-in-out,transform .2s ease-in-out}nav>a.button.secondary:hover{background-color:RGBA(var(--secondary),0.3);transform:translateY(-0.1rem)}form{display:flex;flex-direction:column}form>input{margin:0 0 1rem 0;padding:.5rem 1rem;border:1px solid RGB(var(--white));border-radius:2px;background-color:RGB(var(--darkBlue));color:RGB(var(--white))}form>input:focus{outline:none;border-color:RGB(var(--primary))}form>input.error{border-color:RGB(var(--secondary))}form>button{margin:0;padding:.5rem 1rem;font-weight:bolder;border:transparent;border-radius:2px;background-color:RGB(var(--primary));color:RGB(var(--black))}form>button:focus-visible,form>button:hover{outline:none;background-color:RGBA(var(--primary),0.3);color:RGB(var(--primary))}form>button.disabled{pointer-events:none;opacity:.5}form>button.secondary{background-color:RGB(var(--secondary));color:RGB(var(--black))}form>button.secondary:focus-visible,form>button.secondary:hover{background-color:RGBA(var(--secondary),0.3);color:RGB(var(--secondary))}
|
@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:213,214,130;--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(var(--darkBlue));color:RGB(var(--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(var(--darkBlue),0.7);z-index:2}.app>table{width:100%}header{padding:1rem;background-color:RGBA(var(--darkBlue),0.7)}header>img{margin-bottom:2rem;width:100%;height:auto;text-align:center}header>nav{margin:0;padding:0;height:3rem;display:flex;flex-direction:row;justify-content:center}header>nav>span{width:100%}header>nav>a{margin:auto .25rem;padding:.5rem 1rem;text-decoration:none;white-space:nowrap;color:RGB(var(--primary))}header>nav>a.button{text-decoration:none;background-color:transparent;color:RGB(var(--white));border-radius:2px;transition:background-color .2s ease-in-out,box-shadow .2s ease-in-out,transform .2s ease-in-out}header>nav>a.button:hover{background-color:RGBA(var(--white),0.3);transform:translateY(-0.1rem)}header>nav>a.button.primary{text-decoration:none;background-color:transparent;color:RGB(var(--primary));border-radius:2px;transition:background-color .2s ease-in-out,box-shadow .2s ease-in-out,transform .2s ease-in-out}header>nav>a.button.primary:hover{background-color:RGBA(var(--primary),0.3);transform:translateY(-0.1rem)}header>nav>a.button.secondary{text-decoration:none;background-color:transparent;color:RGB(var(--secondary));border-radius:2px;transition:background-color .2s ease-in-out,box-shadow .2s ease-in-out,transform .2s ease-in-out}header>nav>a.button.secondary:hover{background-color:RGBA(var(--secondary),0.3);transform:translateY(-0.1rem)}.flash{display:flex;flex-direction:column;justify-content:center;align-items:center}.flash>p{margin:.4rem 0 0;padding:.75rem 1rem;width:100%;position:relative;border-left:RGB(var(--secondary)) .25rem solid;background-color:RGB(var(--darkBlue));color:RGB(var(--secondary))}main{padding:1rem;height:100%;display:flex;flex-direction:column}main .center-text{height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center}main .center-text>h2{margin:0;text-align:center;font-size:2em;color:RGB(var(--white))}main .center-text>p{margin:0;text-align:center;font-size:1em;color:RGB(var(--white))}main .auth{margin:auto 0;padding:1rem;display:flex;flex-direction:column;background-color:rgba(var(--darkBlue),0.7);border-radius:2px}main .auth>h2{margin:0 0 1rem 0;font-size:1.3em;color:RGB(var(--white))}main form{display:flex;flex-direction:column}main form>input{margin:0 0 1rem 0;padding:.75rem 1rem;border:1px solid RGB(var(--white));border-radius:2px;background-color:RGB(var(--darkBlue));color:RGB(var(--white))}main form>input:focus{outline:none;border-color:RGB(var(--primary))}main form>input.error{border-color:RGB(var(--secondary))}main form>button{margin:0;padding:.75rem 1rem;font-weight:bolder;border:transparent;border-radius:2px;background-color:RGB(var(--primary));color:RGB(var(--black))}main form>button:focus-visible,main form>button:hover{outline:none;background-color:RGBA(var(--primary),0.3);color:RGB(var(--primary))}main form>button.disabled{pointer-events:none;opacity:.5}main form>button.secondary{background-color:RGB(var(--secondary));color:RGB(var(--black))}main form>button.secondary:focus-visible,main form>button.secondary:hover{background-color:RGBA(var(--secondary),0.3);color:RGB(var(--secondary))}footer{padding:.5rem 1rem;width:100%;display:flex;flex-direction:row;background-color:RGBA(var(--darkBlue),0.7)}footer>p{margin:0;width:100%;text-align:center;font-size:.8em;white-space:nowrap;color:RGB(var(--white))}footer>p>a{color:RGB(var(--secondary));text-decoration:none}footer>p>a:hover{text-decoration:underline}
|
|
@ -58,7 +58,7 @@ body
|
||||||
|
|
||||||
.app
|
.app
|
||||||
margin: 0 auto
|
margin: 0 auto
|
||||||
padding: 2rem
|
padding: 0
|
||||||
|
|
||||||
width: 800px
|
width: 800px
|
||||||
min-height: 100vh
|
min-height: 100vh
|
||||||
|
@ -67,70 +67,24 @@ body
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
|
||||||
background-color: rgba($darkBlue, 0.9)
|
background-color: rgba($darkBlue, 0.7)
|
||||||
z-index: 2
|
z-index: 2
|
||||||
|
|
||||||
> table
|
> table
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
.center-text
|
header
|
||||||
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)
|
|
||||||
|
|
||||||
.auth
|
|
||||||
margin: auto 1rem
|
|
||||||
padding: 1rem
|
padding: 1rem
|
||||||
|
|
||||||
display: flex
|
background-color: RGBA($darkBlue, 0.7)
|
||||||
flex-direction: column
|
|
||||||
|
|
||||||
background-color: rgba($darkBlue, 0.5)
|
> img
|
||||||
border-radius: 2px
|
|
||||||
|
|
||||||
> h2
|
|
||||||
margin: 0 0 1rem 0
|
|
||||||
font-size: 1.3em
|
|
||||||
color: RGB($white)
|
|
||||||
|
|
||||||
.title
|
|
||||||
margin-bottom: 2rem
|
margin-bottom: 2rem
|
||||||
width: 100%
|
width: 100%
|
||||||
height: auto
|
height: auto
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
.subtitle
|
> nav
|
||||||
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
|
margin: 0
|
||||||
padding: 0
|
padding: 0
|
||||||
|
|
||||||
|
@ -161,13 +115,72 @@ nav
|
||||||
&.secondary
|
&.secondary
|
||||||
@include button($secondary)
|
@include button($secondary)
|
||||||
|
|
||||||
|
.flash
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
> p
|
||||||
|
margin: 0.4rem 0 0
|
||||||
|
padding: 0.75rem 1rem
|
||||||
|
|
||||||
|
width: 100%
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
border-left: RGB($secondary) 0.25rem solid
|
||||||
|
background-color: RGB($darkBlue)
|
||||||
|
color: RGB($secondary)
|
||||||
|
|
||||||
|
main
|
||||||
|
padding: 1rem
|
||||||
|
height: 100%
|
||||||
|
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.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)
|
||||||
|
|
||||||
|
.auth
|
||||||
|
margin: auto 0
|
||||||
|
padding: 1rem
|
||||||
|
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
background-color: rgba($darkBlue, 0.7)
|
||||||
|
border-radius: 2px
|
||||||
|
|
||||||
|
> h2
|
||||||
|
margin: 0 0 1rem 0
|
||||||
|
font-size: 1.3em
|
||||||
|
color: RGB($white)
|
||||||
|
|
||||||
form
|
form
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
|
||||||
> input
|
> input
|
||||||
margin: 0 0 1rem 0
|
margin: 0 0 1rem 0
|
||||||
padding: 0.5rem 1rem
|
padding: 0.75rem 1rem
|
||||||
|
|
||||||
border: 1px solid RGB($white)
|
border: 1px solid RGB($white)
|
||||||
border-radius: 2px
|
border-radius: 2px
|
||||||
|
@ -184,7 +197,7 @@ form
|
||||||
|
|
||||||
> button
|
> button
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 0.5rem 1rem
|
padding: 0.75rem 1rem
|
||||||
|
|
||||||
font-weight: bolder
|
font-weight: bolder
|
||||||
|
|
||||||
|
@ -210,3 +223,25 @@ form
|
||||||
&:focus-visible, &:hover
|
&:focus-visible, &:hover
|
||||||
background-color: RGBA($secondary, 0.3)
|
background-color: RGBA($secondary, 0.3)
|
||||||
color: RGB($secondary)
|
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
|
|
@ -13,9 +13,8 @@
|
||||||
|
|
||||||
<div class="auth">
|
<div class="auth">
|
||||||
<h2>Register</h2>
|
<h2>Register</h2>
|
||||||
<form action="" method="POST">
|
<form action="{{ url_for('auth.register') }}" method="POST">
|
||||||
<input type="text" name="username" placeholder="Username" required>
|
<input type="text" name="username" placeholder="Username" required>
|
||||||
<input type="email" name="email" placeholder="Email - Optional">
|
|
||||||
<input type="password" name="password" placeholder="Password" required>
|
<input type="password" name="password" placeholder="Password" required>
|
||||||
<button type="submit" class="secondary">Register</button>
|
<button type="submit" class="secondary">Register</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -12,7 +12,8 @@
|
||||||
<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">
|
<header>
|
||||||
|
<img src="{{ url_for("static", filename="title.png") }}" alt="The Front Rooms logo" class="title" height="60px">
|
||||||
<nav>
|
<nav>
|
||||||
<a href="{{ url_for('views.index', diff=0) }}" class="button">Level 1</a>
|
<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=1) }}" class="button">Level 2</a>
|
||||||
|
@ -22,10 +23,28 @@
|
||||||
|
|
||||||
<span></span> <!-- This is a spacer -->
|
<span></span> <!-- This is a spacer -->
|
||||||
|
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('auth.account') }}" class="button secondary">Account</a>
|
||||||
|
{% else %}
|
||||||
<a href="{{ url_for('auth.auth') }}" class="button secondary">Login</a>
|
<a href="{{ url_for('auth.auth') }}" class="button secondary">Login</a>
|
||||||
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- This is where the flash messages will be displayed -->
|
||||||
|
<div class="flash">
|
||||||
|
{% for message in get_flashed_messages() %}
|
||||||
|
<p>{{ message }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
Loading…
Add table
Reference in a new issue