diff --git a/server/__init__.py b/server/__init__.py index 9b6042e..6f4c05a 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -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, + ) diff --git a/server/api.py b/server/api.py new file mode 100644 index 0000000..cacdf9c --- /dev/null +++ b/server/api.py @@ -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 diff --git a/server/auth.py b/server/auth.py index 2e8e125..357cd9d 100644 --- a/server/auth.py +++ b/server/auth.py @@ -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")) diff --git a/server/models.py b/server/models.py index 3e7c218..aa0d858 100644 --- a/server/models.py +++ b/server/models.py @@ -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, diff --git a/server/static/error-images/1.jpg b/server/static/error-images/1.jpg new file mode 100644 index 0000000..86117b8 Binary files /dev/null and b/server/static/error-images/1.jpg differ diff --git a/server/static/error-images/2.jpg b/server/static/error-images/2.jpg new file mode 100644 index 0000000..f5f7e0a Binary files /dev/null and b/server/static/error-images/2.jpg differ diff --git a/server/static/error-images/3.jpg b/server/static/error-images/3.jpg new file mode 100644 index 0000000..32a3680 Binary files /dev/null and b/server/static/error-images/3.jpg differ diff --git a/server/static/gen/scripts.js b/server/static/gen/scripts.js new file mode 100644 index 0000000..0256e7b --- /dev/null +++ b/server/static/gen/scripts.js @@ -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='';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');} \ No newline at end of file diff --git a/server/static/gen/styles.css b/server/static/gen/styles.css index 9133795..23786f5 100644 --- a/server/static/gen/styles.css +++ b/server/static/gen/styles.css @@ -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: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} \ No newline at end of file +@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:210,206,97;--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);backdrop-filter:blur(5px);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 .7rem;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;border:0 solid transparent;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;border:0 solid transparent;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;border:0 solid transparent;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:0;padding:.75rem 1rem;width:100%;position:relative;background-color:RGB(var(--darkBlue));color:RGB(var(--primary));transition:background-color .2s ease-in-out,padding .2s ease-in-out}.flash>p>span{position:absolute;top:0;left:0;bottom:0;width:.25rem;display:flex;justify-content:center;align-items:center;background-color:RGB(var(--primary));color:transparent;overflow:hidden;transition:width .2s ease-in-out,color .2s ease-in-out,background-color .2s ease-in-out}.flash>p>span>i{font-size:1.25em}.flash>p:hover{padding:.75rem 1rem .75rem 4rem;background-color:RGBA(var(--primary),0.1);cursor:pointer}.flash>p:hover>span{width:3rem;color:RGB(var(--black))}.flash>p.success{color:RGB(var(--primary))}.flash>p.success>span{background-color:RGB(var(--primary))}.flash>p.success:hover{background-color:RGBA(var(--primary),0.1)}.flash>p.error{color:RGB(var(--secondary))}.flash>p.error>span{background-color:RGB(var(--secondary))}.flash>p.error:hover{background-color:RGBA(var(--secondary),0.1)}main{padding:1rem;height:100%;display:flex;flex-direction:column}main>h2{margin:0 0 1rem 0;font-size:1.5em;color:RGB(var(--white))}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 .center-text>img{margin:1rem auto 0;max-width:100%;max-height:15rem;border-radius:2px}main .block{margin-bottom:1rem;padding:1rem;display:flex;flex-direction:column;background-color:rgba(var(--darkBlue),0.7);border-radius:2px}main .block>h2{margin:0 0 .2rem 0;font-size:1.3em;color:RGB(var(--white))}main .block>p{margin:0 0 1rem 0;font-size:1em}main .block .button{margin:0;padding:.5rem .7rem;text-decoration:none;white-space:nowrap;font-size:.9em;color:RGB(var(--primary));text-decoration:none;background-color:RGBA(var(--white),0.02);color:RGB(var(--white));border-radius:2px;border:0 solid transparent;transition:background-color .2s ease-in-out,transform .1s ease-in-out}main .block .button>i{font-size:1.25em;display:block}main .block .button:hover{background-color:RGBA(var(--white),0.3);transform:translateY(-0.1rem)}main .block .button.primary{text-decoration:none;background-color:RGBA(var(--primary),0.02);color:RGB(var(--primary));border-radius:2px;border:0 solid transparent;transition:background-color .2s ease-in-out,transform .1s ease-in-out}main .block .button.primary:hover{background-color:RGBA(var(--primary),0.3);transform:translateY(-0.1rem)}main .block .button.secondary{text-decoration:none;background-color:RGBA(var(--secondary),0.02);color:RGB(var(--secondary));border-radius:2px;border:0 solid transparent;transition:background-color .2s ease-in-out,transform .1s ease-in-out}main .block .button.secondary:hover{background-color:RGBA(var(--secondary),0.3);transform:translateY(-0.1rem)}main .block>table{width:100%;border-collapse:collapse}main .block>table>tbody>tr>td{padding:0 .5rem .5rem 0;text-align:left;font-size:.9em;color:RGB(var(--white));transition:filter .2s ease-in-out}main .block>table>tbody>tr>td:last-child{width:100%}main .block>table>tbody>tr>td.hidden{filter:blur(5px)}main .block.secondary{border:1px solid RGB(var(--secondary))}main .block.secondary>h2{color:RGB(var(--secondary))}main form{display:flex;flex-direction:column}main form>input{margin:0 0 1rem 0;padding:.7rem 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} \ No newline at end of file diff --git a/server/static/js/main.js b/server/static/js/main.js new file mode 100644 index 0000000..be3d32c --- /dev/null +++ b/server/static/js/main.js @@ -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 = ''; + + 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'); +} \ No newline at end of file diff --git a/server/static/style.sass b/server/static/sass/style.sass similarity index 66% rename from server/static/style.sass rename to server/static/sass/style.sass index 753c1f2..bd734bf 100644 --- a/server/static/style.sass +++ b/server/static/sass/style.sass @@ -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 diff --git a/server/templates/account.html b/server/templates/account.html index e2c3e54..5c4d1b7 100644 --- a/server/templates/account.html +++ b/server/templates/account.html @@ -1,10 +1,32 @@ {% extends "base.html" %} +{% block nav %} + +{% endblock %} {% block content %} -
These are your API tokens. Used to link the uploaded scores with your account.
++ | + | {{ token.token }} | +
{{ token.token }}
- {% endfor %} -{% endblock %} \ No newline at end of file + +{% endblock %} diff --git a/server/templates/auth.html b/server/templates/auth.html index 6cdc005..a6b8e7c 100644 --- a/server/templates/auth.html +++ b/server/templates/auth.html @@ -1,7 +1,8 @@ {% extends "base.html" %} {% block content %} -Welcome back!
Don't have an account? Register here!