Add account page and login system

This commit is contained in:
Michał Gdula 2023-05-08 01:00:40 +01:00
parent ebdef07840
commit ffba2b3b7b
11 changed files with 154 additions and 63 deletions

22
run.sh Executable file
View file

@ -0,0 +1,22 @@
#!/bin/sh
# Check if migrations folder exists
if [ ! -d "./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 "No changes detected";
then
echo "No database changes detected"
else
echo "Database changes detected! Migrating..."
flask --app server db migrate
flask --app server db upgrade
fi
# Start server!!!!
echo "Starting server..."
# gunicorn --bind highscore:8080 server:app

View file

@ -1,11 +1,11 @@
import re
from flask import Blueprint, render_template, request, flash, redirect, url_for
from flask_login import current_user, login_required
from werkzeug.security import generate_password_hash
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 cache, login_manager, db
from server.models import Users
from server.extensions import db
from server.models import Users, Tokens
blueprint = Blueprint('auth', __name__)
@ -15,10 +15,12 @@ blueprint = Blueprint('auth', __name__)
def auth():
return render_template('auth.html')
@blueprint.route('/account', methods=['GET'])
@login_required
def account():
return render_template('account.html')
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'])
@ -46,11 +48,44 @@ def register():
flash(err, "error")
return redirect(url_for("auth.auth"))
register_user = Users(
username=username,
password=generate_password_hash(password, method="sha256"),
)
register_user = Users(username=username, password=generate_password_hash(password, method="scrypt"))
db.session.add(register_user)
db.session.commit()
return redirect(url_for("view.index"))
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)
return redirect(url_for("views.index"))
@blueprint.route('/logout', methods=['GET'])
@login_required
def logout():
logout_user()
return redirect(url_for("views.index"))

View file

@ -2,6 +2,7 @@
Database models for the server
"""
from uuid import uuid4
from flask_login import UserMixin
from server.extensions import db
@ -19,17 +20,17 @@ class Scores(db.Model):
username = db.Column(db.String(32), nullable=True)
score = db.Column(db.Float, nullable=False)
difficulty = db.Column(db.String, nullable=False)
difficulty = db.Column(db.Integer, nullable=False)
scored_at = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.utcnow(),
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
"""
@ -39,12 +40,11 @@ class Users(db.Model):
alt_id = db.Column(db.String, nullable=False, unique=True, default=str(uuid4()))
username = db.Column(db.String(32), unique=True, nullable=False)
email = db.Column(db.String, unique=True, nullable=False)
password = db.Column(db.String(128), nullable=False)
password = db.Column(db.String, nullable=False)
joined_at = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.utcnow(), # pylint: disable=E1102
server_default=db.func.now(),
)
scores = db.relationship('Scores', backref='user', lazy=True)
@ -61,10 +61,10 @@ class Tokens(db.Model):
__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, default=str(uuid4()))
created_at = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.utcnow(), # pylint: disable=E1102
server_default=db.func.now(),
)
holder = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)

View file

@ -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: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}
@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:1rem;width:100%;height:auto;text-align:center}nav{margin-top:.3rem;padding:0;display:flex;flex-direction:row;justify-content:center}nav>span{width:100%}nav>a{margin:auto .15rem;padding:.5rem 1rem;text-decoration:none;white-space:nowrap;font-size:.9em;color:RGB(var(--primary))}nav>a.button{text-decoration:none;background-color:RGBA(var(--white),0.02);color:RGB(var(--white));border-radius:2px;transition:background-color .2s ease-in-out,transform .1s 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:RGBA(var(--primary),0.02);color:RGB(var(--primary));border-radius:2px;transition:background-color .2s ease-in-out,transform .1s 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:RGBA(var(--secondary),0.02);color:RGB(var(--secondary));border-radius:2px;transition:background-color .2s ease-in-out,transform .1s ease-in-out}nav>a.button.secondary:hover{background-color:RGBA(var(--secondary),0.3);transform:translateY(-0.1rem)}nav>a>i{font-size:1.25em;display:block}.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-bottom:1rem;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}

View file

@ -9,16 +9,14 @@ $darkBlue: var(--darkBlue)
@mixin button($color)
text-decoration: none
background-color: transparent
background-color: RGBA($color, 0.02)
color: RGB($color)
border-radius: 2px
// box-shadow: 0 0 0 0 RGBA($color, 0)
transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, transform 0.2s ease-in-out
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)
// box-shadow: 0 0.1rem 0.4rem 0.1rem RGBA($color, 0.2)
@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')
@ -79,41 +77,44 @@ header
background-color: RGBA($darkBlue, 0.7)
> img
margin-bottom: 2rem
margin-bottom: 1rem
width: 100%
height: auto
text-align: center
> nav
margin: 0
padding: 0
nav
margin-top: 0.3rem
padding: 0
height: 3rem
display: flex
flex-direction: row
justify-content: center
display: flex
flex-direction: row
justify-content: center
> span
width: 100%
> span
width: 100%
> a
margin: auto 0.15rem
padding: 0.5rem 1rem
> a
margin: auto 0.25rem
padding: 0.5rem 1rem
text-decoration: none
white-space: nowrap
font-size: 0.9em
text-decoration: none
white-space: nowrap
color: RGB($primary)
color: RGB($primary)
&.button
@include button($white)
&.button
@include button($white)
&.primary
@include button($primary)
&.primary
@include button($primary)
&.secondary
@include button($secondary)
&.secondary
@include button($secondary)
> i
font-size: 1.25em
display: block
.flash
display: flex
@ -160,7 +161,7 @@ main
color: RGB($white)
.auth
margin: auto 0
margin-bottom: 1rem
padding: 1rem
display: flex

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,10 @@
{% extends "base.html" %}
{% block content %}
<h2>Hello, {{ current_user.username }}!</h2>
<a href="{{ url_for('auth.logout') }}">Logout</a>
<h2>Tokens</h2>
{% for token in token_list %}
<p>{{ token.token }}</p>
{% endfor %}
{% endblock %}

View file

@ -2,21 +2,19 @@
{% block content %}
<div class="auth">
<h2>Login</h2>
<form action="" method="POST">
<input type="text" name="username" placeholder="Username | Email" required>
<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>
<p style="text-align: center;">or</p>
<div class="auth">
<h2>Register</h2>
<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" class="secondary">Register</button>
<button type="submit">Register</button>
</form>
</div>
{% endblock %}

View file

@ -5,30 +5,33 @@
<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 "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">
<img src="{{ url_for('static', filename='bg.png') }}" alt="The Front Rooms pause menu" class="background">
<div class="app">
<header>
<img src="{{ url_for("static", filename="title.png") }}" alt="The Front Rooms logo" class="title" height="60px">
<img src="{{ url_for('static', filename='title.png') }}" alt="The Front Rooms logo" class="title" height="60px">
<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>
<a href="{{ url_for('views.index') }}" class="button"><i class="ph ph-house"></i></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>
<span></span> <!-- This is a spacer -->
<!-- This is a spacer -->
<span></span>
{% if current_user.is_authenticated %}
<a href="{{ url_for('auth.account') }}" class="button secondary">Account</a>
<a href="{{ url_for('auth.account') }}" class="button primary">{{ current_user.username }}</a>
{% else %}
<a href="{{ url_for('auth.auth') }}" class="button secondary">Login</a>
<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>
<!-- This is where the flash messages will be displayed -->

View file

@ -1,4 +1,16 @@
{% 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>

View file

@ -1,4 +1,4 @@
from flask import Blueprint, jsonify, request, render_template
from flask import Blueprint, request, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField
from wtforms.validators import DataRequired
@ -20,7 +20,7 @@ class ScoreForm(FlaskForm):
@blueprint.route('/')
@cache.cached(timeout=60)
# @cache.cached(timeout=60)
def index():
difficulty = request.args.get('diff', 0)
@ -32,6 +32,11 @@ def index():
return render_template('scores.html', top_scores=top_scores)
@blueprint.route('/about')
def about():
return render_template('about.html')
@blueprint.route('/post', methods=['POST'])
def post():