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 werkzeug.exceptions import HTTPException
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, api
app = Flask(__name__)
app.config.from_pyfile('config.py')
@ -17,14 +19,32 @@ login_manager.init_app(app)
login_manager.login_view = "auth.auth"
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)
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,
)

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 uuid
from flask import Blueprint, render_template, request, flash, redirect, url_for
from flask_login import login_required, login_user, logout_user, current_user
@ -19,6 +20,17 @@ def auth():
@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)
@ -48,7 +60,11 @@ def register():
flash(err, "error")
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.commit()
@ -81,11 +97,5 @@ def login():
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()
flash("Successfully logged in!", "success")
return redirect(url_for("views.index"))

View file

@ -1,7 +1,7 @@
"""
Database models for the server
"""
from uuid import uuid4
import uuid
from flask_login import UserMixin
from server.extensions import db
@ -37,7 +37,7 @@ class Users(db.Model, UserMixin):
__tablename__ = "users"
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)
password = db.Column(db.String, nullable=False)
@ -62,7 +62,7 @@ class Tokens(db.Model):
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()))
token = db.Column(db.String, nullable=False, unique=True)
created_at = db.Column(
db.DateTime,
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)
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
@ -23,7 +24,7 @@ $darkBlue: var(--darkBlue)
\:root
--black: 21, 21, 21
--white: 232, 227, 227
--primary: 213, 214, 130
--primary: 210, 206, 97
--secondary: 185, 77, 77
--gold: 255, 222, 70
--silver: 229, 220, 206
@ -66,6 +67,7 @@ body
flex-direction: column
background-color: rgba($darkBlue, 0.7)
backdrop-filter: blur(5px)
z-index: 2
> table
@ -95,7 +97,7 @@ nav
> a
margin: auto 0.15rem
padding: 0.5rem 1rem
padding: 0.5rem 0.7rem
text-decoration: none
white-space: nowrap
@ -123,15 +125,63 @@ nav
align-items: center
> p
margin: 0.4rem 0 0
margin: 0
padding: 0.75rem 1rem
width: 100%
position: relative
border-left: RGB($secondary) 0.25rem solid
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
padding: 1rem
@ -140,6 +190,11 @@ main
display: flex
flex-direction: column
> h2
margin: 0 0 1rem 0
font-size: 1.5em
color: RGB($white)
.center-text
height: 100%
@ -160,7 +215,13 @@ main
font-size: 1em
color: RGB($white)
.auth
> img
margin: 1rem auto 0
max-width: 100%
max-height: 15rem
border-radius: 2px
.block
margin-bottom: 1rem
padding: 1rem
@ -171,17 +232,68 @@ main
border-radius: 2px
> h2
margin: 0 0 1rem 0
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.75rem 1rem
padding: 0.7rem 1rem
border: 1px solid RGB($white)
border-radius: 2px

View file

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

View file

@ -1,7 +1,8 @@
{% extends "base.html" %}
{% block content %}
<div class="auth">
<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>
@ -9,8 +10,9 @@
</form>
</div>
<div class="auth">
<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>

View file

@ -5,7 +5,13 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Front Rooms Highscores</title>
<script src="https://unpkg.com/@phosphor-icons/web"></script>
{% assets "scripts" %}
<script src="{{ ASSET_URL }}"></script>
{% endassets %}
{% assets "styles" %}
<link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css">
{% endassets %}
@ -13,10 +19,24 @@
<body>
<img src="{{ url_for('static', filename='bg.png') }}" alt="The Front Rooms pause menu" class="background">
<div class="app">
<!-- Get flashed lol -->
<div class="flash">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<p class="{{ category }}" onclick="this.remove()">
<span><i class="ph-bold ph-x"></i></span>
{{ message }}
</p>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<header>
<img src="{{ url_for('static', filename='title.png') }}" alt="The Front Rooms logo" class="title" height="60px">
<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="#" class="button"><i class="ph ph-download-simple"></i></a>
@ -34,13 +54,6 @@
{% block nav %}{% endblock %}
</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 %}
</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.validators import DataRequired
from server.models import Scores, Users, Tokens
from server.extensions import db, cache
from server.models import Scores, Tokens
from server.extensions import db
from server.config import BEARER_TOKEN