Add missing GRID_SIZE option to example env and decrease to 15, add profile page with username and pattern change, add logout, fix clear buttons submitting, add Posts table for later use, prevent timing attacks during pattern equal check

This commit is contained in:
csd4ni3l
2025-10-23 15:51:22 +02:00
parent cc7f906000
commit 810b925387
9 changed files with 382 additions and 15 deletions

View File

@@ -2,4 +2,5 @@ HOST="0.0.0.0"
PORT=8080
DB_FILE="data.db"
APP_KEY="changeme"
DEBUG_MODE=false
DEBUG_MODE=false
GRID_SIZE=15

100
app.py
View File

@@ -25,10 +25,21 @@ def get_db():
db.execute("""
CREATE TABLE IF NOT EXISTS Users (
username TEXT PRIMARY KEY,
pattern TEXT UNIQUE
pattern TEXT NOT NULL
)
""")
db.execute("""
CREATE TABLE IF NOT EXISTS Posts (
id INTEGER PRIMARY KEY,
username TEXT,
comment TEXT,
pattern TEXT NOT NULL
)
""")
db.commit()
return db
@app.teardown_appcontext
@@ -61,7 +72,7 @@ def login():
if flask_login.current_user.is_authenticated:
return redirect(url_for("main"))
return render_template("login.jinja2", grid_size=os.getenv("GRID_SIZE", 25))
return render_template("login.jinja2", grid_size=os.getenv("GRID_SIZE", 15))
elif request.method == "POST":
username = request.form["username"]
@@ -91,7 +102,7 @@ def register():
if flask_login.current_user.is_authenticated:
return redirect(url_for("main"))
return render_template("register.jinja2", grid_size=os.getenv("GRID_SIZE", 25))
return render_template("register.jinja2", grid_size=os.getenv("GRID_SIZE", 15))
elif request.method == "POST":
username = request.form["username"]
@@ -99,7 +110,7 @@ def register():
cur = get_db().cursor()
cur.execute("SELECT username from Users WHERE username = ?", (username, ))
cur.execute("SELECT pattern from Users WHERE username = ?", (username, ))
if cur.fetchone():
cur.close()
@@ -110,5 +121,86 @@ def register():
cur.close()
return redirect(url_for("login"))
@app.route("/posts")
@login_required
def posts():
username = flask_login.current_user.id
return render_template("posts.jinja2", username=username)
@app.route("/profile")
@login_required
def profile():
username = flask_login.current_user.id
return render_template("profile.jinja2", username=username, grid_size=os.getenv("GRID_SIZE", 15), logged_in_account=True)
@app.route("/profile/<username>")
def profile_external(username):
return render_template("profile.jinja2", username=username, grid_size=os.getenv("GRID_SIZE", 15), logged_in_account=False)
@app.route("/change_username", methods=["POST"])
@login_required
def change_username():
username = flask_login.current_user.id
new_username = request.form["new_username"]
cur = get_db().cursor()
cur.execute("UPDATE Users SET username = ? WHERE username = ?", (new_username, username))
get_db().commit()
cur.close()
flask_login.logout_user()
return redirect(url_for("login"))
@app.route("/change_pattern", methods=["POST"])
@login_required
def change_pattern():
username = flask_login.current_user.id
current_pattern, new_pattern = request.form["current_pattern"], request.form["new_pattern"]
cur = get_db().cursor()
cur.execute("SELECT pattern FROM Users WHERE username = ?", (username,))
row = cur.fetchone()
if not row:
cur.close()
return Response("No pattern exists? WTF?", 500)
if not Pattern.from_str(current_pattern) == Pattern.from_json_str(row[0]):
cur.close()
return Response("Invalid Pattern", 401)
cur.execute("UPDATE Users SET pattern = ? WHERE username = ?", (Pattern.from_str(new_pattern).to_json_str(), username))
get_db().commit()
cur.close()
flask_login.logout_user() # not logout redirect because that might fail and we would be in a weird state
return redirect(url_for("login"))
@app.route("/delete_account")
@login_required
def delete_account():
username = flask_login.current_user.id
cur = get_db().cursor()
cur.execute("DELETE FROM Users WHERE username = ?", (username,))
get_db().commit()
cur.close()
flask_login.logout_user() # not logout redirect because that might fail and we would be in a weird state
return redirect(url_for("login"))
@app.route("/logout")
@login_required
def logout():
flask_login.logout_user()
return redirect(url_for("login"))
app.run(host=os.getenv("HOST", "0.0.0.0"), port=int(os.getenv("PORT", 8080)), debug=os.getenv("DEBUG_MODE", False).lower() == "true")

View File

@@ -1,4 +1,6 @@
import json
import json, random, time
systemrandom = random.SystemRandom()
class Pattern():
def __init__(self, data: list[tuple]):
@@ -19,4 +21,6 @@ class Pattern():
if not isinstance(value, Pattern):
return False
time.sleep(systemrandom.uniform(0.001, 0.5)) # prevent timing attacks
return set(self.data) == set(value.data)

View File

@@ -6,6 +6,12 @@
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/posts">Posts</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/profile">Profile</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logout">Logout</a>
</li>

View File

@@ -25,7 +25,7 @@
</div>
<div class="buttons">
<button type="submit" class="btn btn-primary me-2" id="loginBtn">Login</button>
<button id="clearBtn" class="btn btn-danger">Clear</button>
<button type="button" id="clearBtn" class="btn btn-danger">Clear</button>
</div>
</div>
</form>
@@ -75,9 +75,6 @@ function draw(e) {
ctx.fillRect(cellX - CELL_SIZE / 3, cellY - CELL_SIZE / 3, CELL_SIZE, CELL_SIZE);
}
}
else {
console.log(pixel);
}
}
function drawGrid() {

22
templates/posts.jinja2 Normal file
View File

@@ -0,0 +1,22 @@
{% extends "base.jinja2" %}
{% block title %} LoginWeen Posts {% endblock title %}
{% block nav %}
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/posts">Posts</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/profile">Profile</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logout">Logout</a>
</li>
{% endblock %}
{% block body %}
{% endblock body %}

248
templates/profile.jinja2 Normal file
View File

@@ -0,0 +1,248 @@
{% extends "base.jinja2" %}
{% block title %}Loginween Profile{% endblock %}
{% block nav %}
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/posts">Posts</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/profile">Profile</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logout">Logout</a>
</li>
{% endblock %}
{% block body %}
<div class="container my-4">
<div class="card shadow-sm bg-dark text-light border-secondary">
<div class="card-body">
<h2 class="card-title">Profile Overview {% if not logged_in_account %} of {{ username}} {% endif %}</h2>
{% if logged_in_account %}
<p class="mb-1">Logged in as: {{ username }}</p>
{% endif %}
</div>
</div>
{% if logged_in_account %}
<div class="card shadow-sm mt-4 bg-dark text-light border-secondary">
<div class="card-body">
<h4>Change Username</h4>
<form method="POST" action="/change_username">
<div class="mb-3">
<label for="newusername" class="form-label">New Username</label>
<input type="text" class="form-control bg-secondary text-light" id="newusername" name="new_username" placeholder="Enter new username..." required>
</div>
<button type="submit" class="btn btn-primary">Update Username</button>
</form>
</div>
</div>
<div class="card shadow-sm mt-4 bg-dark text-light border-secondary">
<div class="card-body">
<h4>Change Pattern</h4>
<div class="mt-3"></div>
<form id="change_pattern_form" method="POST" action="/change_pattern">
<div class="mb-3 d-block">
<h5>Current</h5>
<input type="hidden" name="current_pattern" id="current_pattern_field">
<canvas id="current_pumpkin_canvas" width="600" height="600" class="my-3"></canvas>
<div class="buttons">
<button type="button" id="currentclearBtn" class="btn btn-danger">Clear</button>
</div>
</div>
<div class="mt-3"></div>
<div class="mb-3 d-block">
<h5>New</h5>
<input type="hidden" name="new_pattern" id="new_pattern_field">
<canvas id="new_pumpkin_canvas" width="600" height="600" class="my-3"></canvas>
<div class="buttons">
<button type="button" id="newclearBtn" class="btn btn-danger">Clear</button>
</div>
</div>
<button type="submit" class="btn btn-primary">Update Pattern</button>
</form>
</div>
</div>
<div class="card shadow-sm mt-4 border-danger bg-dark text-light">
<div class="card-body">
<h4 class="text-danger">Danger Zone</h4>
<p class="text-muted">These actions cannot be undone!</p>
<form method="POST" action="/delete_account" class="d-inline">
<button type="submit" class="btn btn-danger me-2" onclick="return confirm('Are you sure you want to delete your account?');">Delete Account</button>
</form>
</div>
</div>
{% endif %}
</div>
<script type="module">
const canvas = document.getElementById('current_pumpkin_canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = '/static/pumpkin.png';
const GRID_SIZE = {{ grid_size }};
const CELL_SIZE = canvas.width / GRID_SIZE;
img.onload = () => {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
drawGrid();
};
let drawing = false;
let currentPattern = [];
let savedPattern = null;
canvas.addEventListener('mousedown', () => { drawing = true; });
canvas.addEventListener('mouseup', () => { drawing = false; });
canvas.addEventListener('mousemove', draw);
function draw(e) {
if (!drawing) return;
var rect = canvas.getBoundingClientRect();
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
var gridX = Math.floor(x / CELL_SIZE);
var gridY = Math.floor(y / CELL_SIZE);
var cellX = gridX * CELL_SIZE + CELL_SIZE / 3;
var cellY = gridY * CELL_SIZE + CELL_SIZE / 3;
var pixel = ctx.getImageData(cellX, cellY, 1, 1).data;
if (pixel[0] >= 254 && (pixel[1] >= 124 && pixel[1] <= 126)) {
var key = `${gridX},${gridY}`;
if (!currentPattern.includes(key)) {
currentPattern.push(key);
ctx.fillStyle = 'black';
ctx.fillRect(cellX - CELL_SIZE / 3, cellY - CELL_SIZE / 3, CELL_SIZE, CELL_SIZE);
}
}
}
function drawGrid() {
ctx.strokeStyle = 'rgba(0, 0, 0, 0.6)';
ctx.lineWidth = 1;
for (let i = 0; i <= GRID_SIZE; i++) {
const pos = i * CELL_SIZE;
ctx.beginPath();
ctx.moveTo(pos, 0);
ctx.lineTo(pos, canvas.height);
ctx.stroke();
ctx.beginPath()
ctx.moveTo(0, pos);
ctx.lineTo(canvas.width, pos);
ctx.stroke()
}
}
document.getElementById('currentclearBtn').addEventListener('click', clearCanvas);
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
drawGrid()
currentPattern = [];
}
document.getElementById("change_pattern_form").addEventListener('submit', function(event) {
document.getElementById('current_pattern_field').value = JSON.stringify(currentPattern);
});
</script>
<script type="module">
const canvas = document.getElementById('new_pumpkin_canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = '/static/pumpkin.png';
const GRID_SIZE = {{ grid_size }};
const CELL_SIZE = canvas.width / GRID_SIZE;
img.onload = () => {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
drawGrid();
};
let drawing = false;
let currentPattern = [];
let savedPattern = null;
canvas.addEventListener('mousedown', () => { drawing = true; });
canvas.addEventListener('mouseup', () => { drawing = false; });
canvas.addEventListener('mousemove', draw);
function draw(e) {
if (!drawing) return;
var rect = canvas.getBoundingClientRect();
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
var gridX = Math.floor(x / CELL_SIZE);
var gridY = Math.floor(y / CELL_SIZE);
var cellX = gridX * CELL_SIZE + CELL_SIZE / 3;
var cellY = gridY * CELL_SIZE + CELL_SIZE / 3;
var pixel = ctx.getImageData(cellX, cellY, 1, 1).data;
if (pixel[0] >= 254 && (pixel[1] >= 124 && pixel[1] <= 126)) {
var key = `${gridX},${gridY}`;
if (!currentPattern.includes(key)) {
currentPattern.push(key);
ctx.fillStyle = 'black';
ctx.fillRect(cellX - CELL_SIZE / 3, cellY - CELL_SIZE / 3, CELL_SIZE, CELL_SIZE);
}
}
}
function drawGrid() {
ctx.strokeStyle = 'rgba(0, 0, 0, 0.6)';
ctx.lineWidth = 1;
for (let i = 0; i <= GRID_SIZE; i++) {
const pos = i * CELL_SIZE;
ctx.beginPath();
ctx.moveTo(pos, 0);
ctx.lineTo(pos, canvas.height);
ctx.stroke();
ctx.beginPath()
ctx.moveTo(0, pos);
ctx.lineTo(canvas.width, pos);
ctx.stroke()
}
}
document.getElementById('newclearBtn').addEventListener('click', clearCanvas);
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
drawGrid()
currentPattern = [];
}
document.getElementById("change_pattern_form").addEventListener('submit', function(event) {
document.getElementById('new_pattern_field').value = JSON.stringify(currentPattern);
});
</script>
{% endblock body %}

View File

@@ -25,7 +25,7 @@
</div>
<div class="buttons">
<button type="submit" class="btn btn-primary me-2" id="registerBtn">Register</button>
<button id="clearBtn" class="btn btn-danger">Clear</button>
<button type="button" id="clearBtn" class="btn btn-danger">Clear</button>
</div>
</div>
</form>
@@ -75,9 +75,6 @@ function draw(e) {
ctx.fillRect(cellX - CELL_SIZE / 3, cellY - CELL_SIZE / 3, CELL_SIZE, CELL_SIZE);
}
}
else {
console.log(pixel);
}
}
function drawGrid() {

2
uv.lock generated
View File

@@ -86,7 +86,7 @@ wheels = [
[[package]]
name = "loginween"
version = "0.0.1"
source = { editable = "." }
source = { virtual = "." }
dependencies = [
{ name = "flask" },
{ name = "flask-login" },