Merge pull request #14 from Fluffy-Bean/beta

Beta
This commit is contained in:
Michał Gdula 2023-06-24 23:02:00 +01:00 committed by GitHub
commit 2a3dfc5f99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 181 additions and 177 deletions

View file

@ -1,14 +1,15 @@
# syntax=docker/dockerfile:1
FROM alpine:latest
FROM alpine:3.18.2
EXPOSE 8000
RUN apk update && apk add python3 py3-pip postgresql-client
WORKDIR /data
RUN mkdir /storage&& \
apk update && \
apk --no-cache add python3 py3-pip postgresql-client
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
RUN mkdir /storage
COPY ./server ./server
COPY ./run.sh ./run.sh
RUN chmod +x ./run.sh

View file

@ -1,15 +1,15 @@
#!/bin/sh
# Wait for database to start
until pg_isready -d $DB_NAME -h $DB_HOST -U $DB_USER
until pg_isready -d "$DB_NAME" -h "$DB_HOST" -U "$DB_USER"
do
echo "Waiting for database to start... (5s)"
sleep 5
echo "Waiting for database to start... (3s)"
sleep 3
done
echo "Database is ready!"
# Check if migrastions folder exists
# Check if migrations folder exists
if [ ! -d "/data/storage/migrations" ];
then
echo "Creating tables..."
@ -17,7 +17,7 @@ then
fi
# Check if there are any changes to the database
if ! $(flask --app server db check | grep -q "No changes in schema detected.");
if ! flask --app server db check | grep "No changes in schema detected.";
then
echo "Database changes detected! Migrating..."
flask --app server db migrate

View file

@ -21,160 +21,165 @@ from .extensions import db
blueprint = Blueprint("account", __name__, url_prefix="/account")
@blueprint.route("/settings", methods=["GET", "POST"])
@blueprint.route("/settings", methods=["GET"])
@login_required
def settings():
if request.method == "POST":
username = request.form.get("username", "").strip()
email = request.form.get("email", "").strip()
password = request.form.get("password", "").strip()
error = []
def get_settings():
action = request.args.get("action", None)
user = Users.query.filter_by(username=current_user.username).first()
if action == "logout":
logout_user()
flash("Successfully logged out!", "success")
return redirect(url_for("views.index"))
if not check_password_hash(user.password, password):
flash("Password is incorrect!", "error")
return redirect(url_for("account.settings"))
sessions = Sessions.query.filter_by(user_id=current_user.id).all()
return render_template("views/account_settings.html", sessions=sessions)
if "file" in request.files and request.files["file"].filename:
picture = request.files["file"]
file_ext = picture.filename.split(".")[-1].lower()
file_name = f"{user.id}.{file_ext}"
if file_ext not in UPLOAD_EXTENSIONS:
error.append("Picture is not a valid image!")
if picture.content_length > UPLOAD_MAX_SIZE:
error.append(
f"Picture must be less than {UPLOAD_EXTENSIONS / 1000000}MB!"
)
@blueprint.route("/settings", methods=["POST"])
@login_required
def post_settings():
username = request.form.get("username", "").strip()
email = request.form.get("email", "").strip()
password = request.form.get("password", "").strip()
error = []
image = Image.open(picture.stream)
user = Users.query.filter_by(username=current_user.username).first()
# Resizing gifs is more work than it's worth
if file_ext != "gif":
image_x, image_y = image.size
image.thumbnail(
(min(image_x, UPLOAD_RESOLUTION), min(image_y, UPLOAD_RESOLUTION))
)
if error:
for err in error:
flash(err, "error")
return redirect(url_for("account.settings"))
if user.picture:
os.remove(os.path.join(UPLOAD_DIR, user.picture))
user.picture = file_name
if file_ext == "gif":
image.save(os.path.join(UPLOAD_DIR, file_name), save_all=True)
else:
image.save(os.path.join(UPLOAD_DIR, file_name))
image.close()
if username:
if USER_REGEX.match(username):
user.username = username
else:
error.append("Username is invalid!")
if email:
if USER_EMAIL_REGEX.match(email):
user.email = email
else:
error.append("Email is invalid!")
if error:
for err in error:
flash(err, "error")
return redirect(url_for("account.settings"))
db.session.commit()
flash("Successfully updated account!", "success")
if not check_password_hash(user.password, password):
flash("Password is incorrect!", "error")
return redirect(url_for("account.settings"))
else:
action = request.args.get("action", None)
if action == "logout":
logout_user()
flash("Successfully logged out!", "success")
return redirect(url_for("views.index"))
if "file" in request.files and request.files["file"].filename:
picture = request.files["file"]
file_ext = picture.filename.split(".")[-1].lower()
file_name = f"{user.id}.{file_ext}"
sessions = Sessions.query.filter_by(user_id=current_user.id).all()
return render_template("views/account_settings.html", sessions=sessions)
if file_ext not in UPLOAD_EXTENSIONS:
error.append("Picture is not a valid image!")
if picture.content_length > UPLOAD_MAX_SIZE:
error.append(f"Picture must be less than {UPLOAD_MAX_SIZE}MB!")
image = Image.open(picture.stream)
@blueprint.route("/reset-password", methods=["GET", "POST"])
@login_required
def password_reset():
if request.method == "POST":
current = request.form.get("current", "").strip()
new = request.form.get("new", "").strip()
confirm = request.form.get("confirm", "").strip()
error = []
user = Users.query.filter_by(username=current_user.username).first()
if not current or not new or not confirm:
error.append("Please fill out all fields!")
if not check_password_hash(user.password, current):
error.append("Current password is incorrect!")
if len(new) < 8:
error.append(
"New password is too short! Must be at least 8 characters long."
# Resizing gifs is more work than it's worth
if file_ext != "gif":
image_x, image_y = image.size
image.thumbnail(
(min(image_x, UPLOAD_RESOLUTION), min(image_y, UPLOAD_RESOLUTION))
)
if new != confirm:
error.append("New passwords do not match!")
if error:
for err in error:
flash(err, "error")
return redirect(url_for("account.password_reset"))
return redirect(url_for("account.settings"))
user.password = generate_password_hash(new, method="scrypt")
user.alt_id = str(uuid.uuid4())
db.session.commit()
if user.picture:
os.remove(os.path.join(UPLOAD_DIR, user.picture))
flash("Successfully changed password!", "success")
logout_user()
return redirect(url_for("auth.auth"))
else:
return render_template("views/reset_password.html")
user.picture = file_name
if file_ext == "gif":
image.save(os.path.join(UPLOAD_DIR, file_name), save_all=True)
else:
image.save(os.path.join(UPLOAD_DIR, file_name))
image.close()
if username:
if USER_REGEX.match(username):
user.username = username
else:
error.append("Username is invalid!")
if email:
if USER_EMAIL_REGEX.match(email):
user.email = email
else:
error.append("Email is invalid!")
if error:
for err in error:
flash(err, "error")
return redirect(url_for("account.settings"))
db.session.commit()
flash("Successfully updated account!", "success")
return redirect(url_for("account.settings"))
@blueprint.route("/delete-account", methods=["GET", "POST"])
@blueprint.route("/password", methods=["GET"])
@login_required
def delete_account():
if request.method == "POST":
username = request.form.get("username", "").strip()
password = request.form.get("password", "").strip()
error = []
def get_password_reset():
return render_template("views/reset_password.html")
user = Users.query.filter_by(username=current_user.username).first()
if username != user.username:
error.append("Username does not match!")
if not password:
error.append("Please fill out all fields!")
if not check_password_hash(user.password, password):
error.append("Password is incorrect!")
@blueprint.route("/password", methods=["POST"])
@login_required
def post_password_reset():
current = request.form.get("current", "").strip()
new = request.form.get("new", "").strip()
confirm = request.form.get("confirm", "").strip()
error = []
if error:
for err in error:
flash(err, "error")
return redirect(url_for("account.delete_account"))
user = Users.query.filter_by(username=current_user.username).first()
db.session.query(Sessions).filter_by(user_id=current_user.id).delete()
db.session.query(Scores).filter_by(user_id=current_user.id).delete()
db.session.query(ProfileTags).filter_by(user_id=current_user.id).delete()
db.session.query(PasswordReset).filter_by(user_id=current_user.id).delete()
db.session.delete(user)
db.session.commit()
if not current or not new or not confirm:
error.append("Please fill out all fields!")
if not check_password_hash(user.password, current):
error.append("Current password is incorrect!")
if len(new) < 8:
error.append("New password is too short! Must be at least 8 characters long.")
if new != confirm:
error.append("New passwords do not match!")
flash("Successfully deleted account!", "success")
logout_user()
return redirect(url_for("auth.auth"))
else:
return render_template("views/delete_account.html")
if error:
for err in error:
flash(err, "error")
return redirect(url_for("account.password_reset"))
user.password = generate_password_hash(new, method="scrypt")
user.alt_id = str(uuid.uuid4())
db.session.commit()
flash("Successfully changed password!", "success")
logout_user()
return redirect(url_for("auth.auth"))
@blueprint.route("/delete-account", methods=["GET"])
@login_required
def get_delete_account():
return render_template("views/delete_account.html")
@blueprint.route("/delete", methods=["POST"])
@login_required
def post_delete_account():
username = request.form.get("username", "").strip()
password = request.form.get("password", "").strip()
error = []
user = Users.query.filter_by(username=current_user.username).first()
if username != user.username:
error.append("Username does not match!")
if not password:
error.append("Please fill out all fields!")
if not check_password_hash(user.password, password):
error.append("Password is incorrect!")
if error:
for err in error:
flash(err, "error")
return redirect(url_for("account.delete_account"))
db.session.query(Sessions).filter_by(user_id=current_user.id).delete()
db.session.query(Scores).filter_by(user_id=current_user.id).delete()
db.session.query(ProfileTags).filter_by(user_id=current_user.id).delete()
db.session.query(PasswordReset).filter_by(user_id=current_user.id).delete()
db.session.delete(user)
db.session.commit()
flash("Successfully deleted account!", "success")
logout_user()
return redirect(url_for("auth.auth"))

View file

@ -6,7 +6,7 @@ SECRET_KEY = getenv("FLASK_KEY")
UPLOAD_DIR = "/data/uploads"
UPLOAD_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp"]
UPLOAD_RESOLUTION = 512
UPLOAD_RESOLUTION = 169
UPLOAD_MAX_SIZE = 3 * 1024 * 1024 # 3MB
GAME_VERSION = "alpha"

View file

@ -1,10 +1,10 @@
function addFlashMessage(message, type='success') {
let flask = document.createElement('p');
const flask = document.createElement('p');
flask.onclick = () => flask.remove();
flask.classList.add(type);
flask.innerHTML = message;
let close = document.createElement('span');
const close = document.createElement('span');
close.innerHTML = '<i class="ph-bold ph-x"></i>';
flask.appendChild(close);

View file

@ -1,43 +1,43 @@
function showHint() {
let search = document.querySelector('#search');
let searchPos = search.getBoundingClientRect();
let hint = document.querySelector('.search-hint');
const search = document.querySelector('#search');
const searchPos = search.getBoundingClientRect();
const hint = document.querySelector('.search-hint');
hint.style.width = search.offsetWidth + 'px';
hint.style.left = searchPos.left + 'px';
hint.style.top = searchPos.bottom + 'px';
hint.style.width = `${search.offsetWidth}px`;
hint.style.left = `${searchPos.left}px`;
hint.style.top = `${searchPos.bottom}px`;
hint.style.display = 'flex';
}
function hideHint() {
let hint = document.querySelector('.search-hint');
const hint = document.querySelector('.search-hint');
hint.style.display = 'none';
}
function updateHint() {
let search = document.querySelector('#search');
let searchPos = search.getBoundingClientRect();
let hint = document.querySelector('.search-hint');
const search = document.querySelector('#search');
const searchPos = search.getBoundingClientRect();
const hint = document.querySelector('.search-hint');
hint.style.width = search.offsetWidth + 'px';
hint.style.left = searchPos.left + 'px';
hint.style.top = searchPos.bottom + 'px';
hint.style.width = `${search.offsetWidth}px`;
hint.style.left = `${searchPos.left}px`;
hint.style.top = `${searchPos.bottom}px`;
}
function getSearch() {
let search = document.querySelector('#search').value;
let hint = document.querySelector('.search-hint');
const hint = document.querySelector('.search-hint');
if (search.length === 0) {
hint.innerHTML = '<p>Start typing to see results...</p>';
return;
}
fetch('/api/search?q=' + search.toString(), {
fetch(`/api/search?q=${search}`, {
method: 'GET',
})
.then(response => response.json())
@ -50,12 +50,11 @@ function getSearch() {
hint.innerHTML = '';
data.forEach(user => {
let el = document.createElement('button');
const el = document.createElement('button');
el.innerHTML = user;
el.onmousedown = function (event) {
event.preventDefault();
search = document.querySelector('#search');
search.value = user.toString();
search = user.toString();
search.blur();
}
hint.appendChild(el);
@ -69,7 +68,7 @@ function getSearch() {
window.onload = () => {
let typingTimer;
let search = document.querySelector('#search');
const search = document.querySelector('#search');
if (search === null) {
return;

View file

@ -1,6 +1,6 @@
function yeetSession(id) {
let form = new FormData();
form.append('session_id', id);
const form = new FormData();
form.append('session', id);
fetch('/api/tokens', {
method: 'POST',

View file

@ -5,7 +5,7 @@
<span class="spacer"></span>
{% if current_user.is_authenticated %}
<a href="{{ url_for('account.settings') }}" class="button primary">
<a href="{{ url_for('account.get_settings') }}" class="button primary">
{{ current_user.username }}
{% if not current_user.email %}<i class="ph ph-warning" style="margin-left: 0.2rem !important;"></i>{% endif %}
</a>

View file

@ -4,7 +4,7 @@
{% block content %}
<div class="block">
<h2 style="margin-bottom: 1rem;">Profile Settings</h2>
<form action="{{ url_for('account.settings') }}" method="POST" enctype="multipart/form-data">
<form action="{{ url_for('account.post_settings') }}" method="POST" enctype="multipart/form-data">
<div class="profile-settings">
<div class="picture">
{% if current_user.picture %}
@ -34,7 +34,7 @@
<div class="table">
<table>
<tr>
<th></th>
<th>Options</th>
<th>Device</th>
<th>Created</th>
<th>Last Used</th>
@ -44,7 +44,7 @@
<td><button onclick="yeetSession({{ session.id }})" class="button secondary"><i class="ph ph-trash"></i></button></td>
<td>{{ session.device_type }}</td>
<td>{{ session.created_at.strftime('%Y-%m-%d') }}</td>
<td>{{ session.last_used.strftime('%Y-%m-%d') }}</td>
<td>{{ session.last_used|timesince }}</td>
</tr>
{% endfor %}
</table>
@ -54,9 +54,9 @@
<div class="block secondary">
<h2>Danger Zone</h2>
<p>Be careful!</p>
<a href="{{ url_for('account.delete_account') }}" class="button secondary" style="margin-bottom: 0.5rem">Delete Account</a>
<a href="{{ url_for('account.password_reset') }}" class="button secondary" style="margin-bottom: 0.5rem">Reset Password</a>
<a href="{{ url_for('account.settings', action='logout') }}" class="button secondary">Logout</a>
<a href="{{ url_for('account.get_delete_account') }}" class="button secondary" style="margin-bottom: 0.5rem">Delete Account</a>
<a href="{{ url_for('account.get_password_reset') }}" class="button secondary" style="margin-bottom: 0.5rem">Reset Password</a>
<a href="{{ url_for('account.get_settings', action='logout') }}" class="button secondary">Logout</a>
</div>
@ -67,8 +67,7 @@
// Adjusted from https://stackoverflow.com/a/3814285/14885829
document.getElementById('profile-picture').onchange = (event) => {
let tgt = event.target || window.event.srcElement,
files = tgt.files;
let files = event.target.files;
if (FileReader && files && files.length) {
let fr = new FileReader();
@ -78,7 +77,7 @@
fr.readAsDataURL(files[0]);
}
else {
addFlashMessage("Your browser could not show a preview of your profile picture!", "error")
addFlashMessage("Your browser could not show a preview of your profile picture!", "error");
}
}
</script>

View file

@ -8,7 +8,7 @@
Deleting your account will delete <span style="color: rgb(var(--secondary)) !important;">EVERYTHING</span> on your account, including <span style="color: rgb(var(--secondary)) !important;">ALL</span> your ever submitted scores.
There is <span style="color: rgb(var(--secondary)) !important;">NO WAY</span> to recover your account from this, are you sure you want todo this?
</p>
<form action="{{ url_for('account.delete_account') }}" method="POST">
<form action="{{ url_for('account.post_delete_account') }}" method="POST">
{{ text(id="username", name="username", required=True) }}
{{ text(id="password", name="password", type="password", required=True) }}
<button type="submit" class="button secondary">Delete account forever</button>

View file

@ -5,7 +5,7 @@
<div class="block secondary">
<h2>Password Reset</h2>
<p>Forgotten your current password? Go here [insert password reset tool link]</p>
<form action="{{ url_for('account.password_reset') }}" method="POST">
<form action="{{ url_for('account.post_password_reset') }}" method="POST">
{{ text(id="current-password", name="current", type="password", required=True) }}
{{ text(id="new-password", name="new", type="password", required=True) }}
{{ text(id="confirm-password", name="confirm", type="password", required=True) }}

View file

@ -9,7 +9,7 @@ from .extensions import db
blueprint = Blueprint("views", __name__)
@blueprint.route("/")
@blueprint.route("/", methods=["GET"])
def index():
diff_arg = request.args.get("diff", 0)
ver_arg = request.args.get("ver", GAME_VERSION).strip()
@ -45,6 +45,6 @@ def index():
)
@blueprint.route("/about")
@blueprint.route("/about", methods=["GET"])
def about():
return render_template("views/about.html")