mirror of
https://github.com/Project-Redacted/Highscores-Server.git
synced 2025-05-30 06:53:13 +00:00
Add functionality to tokens
Style more elements
This commit is contained in:
parent
ffba2b3b7b
commit
10456f60a0
16 changed files with 380 additions and 42 deletions
|
@ -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
39
server/api.py
Normal 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
|
|
@ -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"))
|
||||
|
|
|
@ -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,
|
||||
|
|
BIN
server/static/error-images/1.jpg
Normal file
BIN
server/static/error-images/1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 400 KiB |
BIN
server/static/error-images/2.jpg
Normal file
BIN
server/static/error-images/2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 165 KiB |
BIN
server/static/error-images/3.jpg
Normal file
BIN
server/static/error-images/3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 708 KiB |
6
server/static/gen/scripts.js
Normal file
6
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');}
|
File diff suppressed because one or more lines are too long
106
server/static/js/main.js
Normal file
106
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');
|
||||
}
|
|
@ -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
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
8
server/templates/error.html
Normal file
8
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 %}
|
|
@ -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
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue