Add functionality to tokens

Style more elements
This commit is contained in:
Michał Gdula 2023-05-08 22:02:01 +01:00
parent ffba2b3b7b
commit 10456f60a0
16 changed files with 380 additions and 42 deletions

View file

@ -1,8 +1,10 @@
from flask import Flask from random import randint
from flask import Flask, render_template, abort
from flask_assets import Bundle from flask_assets import Bundle
from werkzeug.exceptions import HTTPException
from server.extensions import db, migrate, cache, assets, login_manager from server.extensions import db, migrate, cache, assets, login_manager
from server.models import Users from server.models import Users
from server import views, auth from server import views, auth, api
app = Flask(__name__) app = Flask(__name__)
app.config.from_pyfile('config.py') app.config.from_pyfile('config.py')
@ -17,14 +19,32 @@ login_manager.init_app(app)
login_manager.login_view = "auth.auth" 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")
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) 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)
app.register_blueprint(api.blueprint)
@login_manager.user_loader @login_manager.user_loader
def load_user(user_id): def load_user(user_id):
return Users.query.filter_by(alt_id=user_id).first() 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,
)

39
server/api.py Normal file
View file

@ -0,0 +1,39 @@
import uuid
from flask import Blueprint, request, jsonify
from flask_login import login_required, current_user
from server.models import Tokens
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

View file

@ -1,4 +1,5 @@
import re import re
import uuid
from flask import Blueprint, render_template, request, flash, redirect, url_for from flask import Blueprint, render_template, request, flash, redirect, url_for
from flask_login import login_required, login_user, logout_user, current_user from flask_login import login_required, login_user, logout_user, current_user
@ -19,6 +20,17 @@ def auth():
@blueprint.route('/account', methods=['GET']) @blueprint.route('/account', methods=['GET'])
@login_required @login_required
def account(): 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() token_list = Tokens.query.filter_by(holder=current_user.id).all()
return render_template('account.html', token_list=token_list) return render_template('account.html', token_list=token_list)
@ -48,7 +60,11 @@ def register():
flash(err, "error") flash(err, "error")
return redirect(url_for("auth.auth")) return redirect(url_for("auth.auth"))
register_user = Users(username=username, password=generate_password_hash(password, method="scrypt")) register_user = Users(
alt_id=str(uuid.uuid4()),
username=username,
password=generate_password_hash(password, method="scrypt")
)
db.session.add(register_user) db.session.add(register_user)
db.session.commit() db.session.commit()
@ -81,11 +97,5 @@ def login():
return redirect(url_for("auth.account")) return redirect(url_for("auth.account"))
login_user(user, remember=True) login_user(user, remember=True)
return redirect(url_for("views.index")) flash("Successfully logged in!", "success")
@blueprint.route('/logout', methods=['GET'])
@login_required
def logout():
logout_user()
return redirect(url_for("views.index")) return redirect(url_for("views.index"))

View file

@ -1,7 +1,7 @@
""" """
Database models for the server Database models for the server
""" """
from uuid import uuid4 import uuid
from flask_login import UserMixin from flask_login import UserMixin
from server.extensions import db from server.extensions import db
@ -37,7 +37,7 @@ class Users(db.Model, UserMixin):
__tablename__ = "users" __tablename__ = "users"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
alt_id = db.Column(db.String, nullable=False, unique=True, default=str(uuid4())) alt_id = db.Column(db.String, nullable=False, unique=True)
username = db.Column(db.String(32), unique=True, nullable=False) username = db.Column(db.String(32), unique=True, nullable=False)
password = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False)
@ -62,7 +62,7 @@ class Tokens(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
holder = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) holder = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
token = db.Column(db.String, nullable=False, unique=True, default=str(uuid4())) token = db.Column(db.String, nullable=False, unique=True)
created_at = db.Column( created_at = db.Column(
db.DateTime, db.DateTime,
nullable=False, nullable=False,

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

106
server/static/js/main.js Normal file
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

@ -12,6 +12,7 @@ $darkBlue: var(--darkBlue)
background-color: RGBA($color, 0.02) background-color: RGBA($color, 0.02)
color: RGB($color) color: RGB($color)
border-radius: 2px border-radius: 2px
border: 0 solid transparent
transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out
&:hover &:hover
@ -23,7 +24,7 @@ $darkBlue: var(--darkBlue)
\:root \:root
--black: 21, 21, 21 --black: 21, 21, 21
--white: 232, 227, 227 --white: 232, 227, 227
--primary: 213, 214, 130 --primary: 210, 206, 97
--secondary: 185, 77, 77 --secondary: 185, 77, 77
--gold: 255, 222, 70 --gold: 255, 222, 70
--silver: 229, 220, 206 --silver: 229, 220, 206
@ -66,6 +67,7 @@ body
flex-direction: column flex-direction: column
background-color: rgba($darkBlue, 0.7) background-color: rgba($darkBlue, 0.7)
backdrop-filter: blur(5px)
z-index: 2 z-index: 2
> table > table
@ -95,7 +97,7 @@ nav
> a > a
margin: auto 0.15rem margin: auto 0.15rem
padding: 0.5rem 1rem padding: 0.5rem 0.7rem
text-decoration: none text-decoration: none
white-space: nowrap white-space: nowrap
@ -123,15 +125,63 @@ nav
align-items: center align-items: center
> p > p
margin: 0.4rem 0 0 margin: 0
padding: 0.75rem 1rem padding: 0.75rem 1rem
width: 100% width: 100%
position: relative position: relative
border-left: RGB($secondary) 0.25rem solid
background-color: RGB($darkBlue) background-color: RGB($darkBlue)
color: RGB($secondary) 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 main
padding: 1rem padding: 1rem
@ -140,6 +190,11 @@ main
display: flex display: flex
flex-direction: column flex-direction: column
> h2
margin: 0 0 1rem 0
font-size: 1.5em
color: RGB($white)
.center-text .center-text
height: 100% height: 100%
@ -160,7 +215,13 @@ main
font-size: 1em font-size: 1em
color: RGB($white) color: RGB($white)
.auth > img
margin: 1rem auto 0
max-width: 100%
max-height: 15rem
border-radius: 2px
.block
margin-bottom: 1rem margin-bottom: 1rem
padding: 1rem padding: 1rem
@ -171,17 +232,68 @@ main
border-radius: 2px border-radius: 2px
> h2 > h2
margin: 0 0 1rem 0 margin: 0 0 0.2rem 0
font-size: 1.3em font-size: 1.3em
color: RGB($white) 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 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.75rem 1rem padding: 0.7rem 1rem
border: 1px solid RGB($white) border: 1px solid RGB($white)
border-radius: 2px border-radius: 2px

View file

@ -1,10 +1,32 @@
{% extends "base.html" %} {% 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 %} {% block content %}
<h2>Hello, {{ current_user.username }}!</h2> <div class="block">
<a href="{{ url_for('auth.logout') }}">Logout</a> <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>
<h2>Tokens</h2> <div class="block secondary">
{% for token in token_list %} <h2>Danger Zone</h2>
<p>{{ token.token }}</p> <p>These actions are irreversible. Be careful!</p>
{% endfor %} <a href="{{ url_for('auth.account', action='delete') }}" class="button secondary">Delete Account</a>
{% endblock %} <a href="{{ url_for('auth.account', action='password') }}" class="button secondary">Reset Password</a>
</div>
{% endblock %}

View file

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

View file

@ -5,7 +5,13 @@
<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> <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 %}
@ -13,10 +19,24 @@
<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">
<!-- 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> <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> <nav>
<a href="{{ url_for('views.index') }}" class="button"><i class="ph ph-house"></i></a> <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="{{ 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> <a href="#" class="button"><i class="ph ph-download-simple"></i></a>
@ -34,13 +54,6 @@
{% block nav %}{% endblock %} {% block nav %}{% endblock %}
</header> </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> <main>
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>

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

@ -3,8 +3,8 @@ from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField from wtforms import StringField, IntegerField
from wtforms.validators import DataRequired from wtforms.validators import DataRequired
from server.models import Scores, Users, Tokens from server.models import Scores, Tokens
from server.extensions import db, cache from server.extensions import db
from server.config import BEARER_TOKEN from server.config import BEARER_TOKEN