added user accounts and dashboard #56

Merged
Ugric merged 1 commits from TommyHEC/Komodo-Hub:main into main 2026-03-10 16:34:43 +00:00
10 changed files with 230 additions and 15 deletions

No files matched your search

Binary file not shown.
+44 -1
View File
@@ -1,5 +1,48 @@
from datetime import datetime
from database.db_connection import get_db
from database.db_connection import get_db, get_accounts_db
import hashlib
import os
def _hash_password(password: str, salt: str = None):
if salt is None:
salt = os.urandom(16).hex()
pw_hash = hashlib.sha256((salt + password).encode()).hexdigest()
return f"{salt}${pw_hash}"
def _verify_password(stored: str, password: str) -> bool:
salt, _ = stored.split("$", 1)
return stored == _hash_password(password, salt)
def register_user(username, email, password, account_type="private_user"):
with get_accounts_db() as conn:
existing = conn.execute(
"SELECT user_id FROM users WHERE username = ? OR email = ?",
(username, email)
).fetchone()
if existing:
raise ValueError("A user with that username or email already exists.")
password_hash = _hash_password(password)
conn.execute("""
INSERT INTO users (username, email, password_hash, account_type, created_at)
VALUES (?, ?, ?, ?, ?)
""", (username, email, password_hash, account_type, datetime.now()))
def login_user(username, password):
with get_accounts_db() as conn:
row = conn.execute(
"SELECT * FROM users WHERE username = ?",
(username,)
).fetchone()
if not row:
raise ValueError("Invalid username or password.")
user = dict(row)
if not _verify_password(user["password_hash"], password):
raise ValueError("Invalid username or password.")
return {k: user[k] for k in ("user_id", "username", "email", "account_type")}
def add_species(english, latin, body, category, extinction_risk, image_id):
+22
View File
@@ -2,11 +2,33 @@ import sqlite3
from contextlib import contextmanager
DATABASE_PATH = "database/database.db"
ACCOUNTS_DATABASE_PATH = "database/accounts.db"
@contextmanager
def get_db():
conn = sqlite3.connect(DATABASE_PATH)
conn.row_factory = sqlite3.Row
try:
yield conn
conn.commit()
finally:
conn.close()
@contextmanager
def get_accounts_db():
conn = sqlite3.connect(ACCOUNTS_DATABASE_PATH)
conn.row_factory = sqlite3.Row
conn.execute("""
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
account_type TEXT NOT NULL DEFAULT 'private_user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
try:
yield conn
conn.commit()
+18 -2
View File
@@ -1,4 +1,4 @@
from database.db_connection import get_db
from database.db_connection import get_db, get_accounts_db
from datetime import datetime
SEED_SPECIES = [
@@ -136,5 +136,21 @@ def seed_species():
print("Seeding complete.")
def init_accounts_database():
with get_accounts_db() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
account_type TEXT NOT NULL DEFAULT 'private_user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
print("Accounts database initialised.")
if __name__ == "__main__":
init_database()
init_database()
init_accounts_database()
+56 -6
View File
@@ -1,11 +1,61 @@
from flask import render_template, Blueprint, request, redirect, url_for
from flask import render_template, Blueprint, request, redirect, url_for, session
from database.db_commands import register_user, login_user
page = Blueprint("login", __name__, url_prefix="")
@page.route("/login")
def login():
return render_template("accounts/login.jinja")
@page.route("/register")
@page.route("/login", methods=["GET", "POST"])
def login():
if "user_id" in session:
return redirect(url_for("login.dashboard"))
error = None
if request.method == "POST":
username = request.form.get("username", "").strip()
password = request.form.get("password", "")
try:
user = login_user(username, password)
session.clear()
session["user_id"] = user["user_id"]
session["username"] = user["username"]
session["account_type"] = user["account_type"]
return redirect(url_for("login.dashboard"))
except ValueError as e:
error = str(e)
return render_template("accounts/login.jinja", error=error)
@page.route("/register", methods=["GET", "POST"])
def register():
return render_template("accounts/user_registration.jinja")
if "user_id" in session:
return redirect(url_for("login.dashboard"))
error = None
if request.method == "POST":
username = request.form.get("username", "").strip()
email = request.form.get("email", "").strip()
account_type = request.form.get("account_type", "private_user")
password = request.form.get("password", "")
password_confirm = request.form.get("password_confirm", "")
if password != password_confirm:
error = "Passwords do not match."
elif len(password) < 8:
error = "Password must be at least 8 characters."
else:
try:
register_user(username, email, password, account_type)
return redirect(url_for("login.login"))
except ValueError as e:
error = str(e)
return render_template("accounts/user_registration.jinja", error=error)
@page.route("/logout")
def logout():
session.clear()
return redirect(url_for("login.login"))
@page.route("/dashboard")
def dashboard():
if "user_id" not in session:
return redirect(url_for("login.login"))
return render_template("accounts/dashboard.jinja")
+68
View File
@@ -0,0 +1,68 @@
{% extends "base.jinja" %}
{% block title %}Dashboard{% endblock %}
{% block head %}{% endblock %}
{% block content %}
{% from 'button.jinja' import button %}
<section id="hero" style="padding: 40px 20px; text-align: center;">
<h1>Welcome back, {{ session.username }}.</h1>
<p style="max-width: 700px; margin: 0.5rem auto;">Your conservation dashboard.</p>
</section>
<section style="padding: 0 20px 40px 20px; max-width: 900px; margin: 0 auto;">
{#Enrolled Programmes (everyone sees this)#}
<div class="container-full" style="margin-bottom: 20px;">
<h2>Your Programmes</h2>
{% if enrolled_programmes %}
<div class="species-grid">
{% for prog in enrolled_programmes %}
<div class="container-lite">
<h3>{{ prog.name }}</h3>
<p>{{ prog.description }}</p>
{{ button(url='/programmes/' ~ prog.id, button_text='View', extra_classes='homepage-btn') }}
</div>
{% endfor %}
</div>
{% else %}
<p style="color: var(--text); opacity: 0.6;">You are not enrolled in any programmes yet.</p>
{{ button(url='/programmes', button_text='Browse Programmes', extra_classes='homepage-btn') }}
{% endif %}
</div>
{#Student#}
{% if session.account_type == "student" %}
<div class="container-full" style="margin-bottom: 20px;">
<h2>Classes</h2>
<p>View the classes you have joined.</p>
{{ button(url='/classes', button_text='View Classes', extra_classes='homepage-btn') }}
</div>
{% endif %}
{#Teacher#}
{% if session.account_type == "teacher" %}
<div class="container-full" style="margin-bottom: 20px;">
<h2>Classes</h2>
<p>View your classes or set up a new one for your students.</p>
<div class="homepage-buttons" style="justify-content: flex-start; margin-top: 1rem;">
{{ button(url='/classes', button_text='View Classes', extra_classes='homepage-btn') }}
</div>
</div>
{% endif %}
{#Community Leader#}
{% if session.account_type == "community_leader" %}
<div class="container-full" style="margin-bottom: 20px;">
<h2>Programmes</h2>
<p>Create a new conservation programme for others to join.</p>
{{ button(url='/programmes/create', button_text='Create a Programme', extra_classes='homepage-btn') }}
</div>
{% endif %}
</section>
<section style="padding: 20px; text-align: center;">
{{ button(url='/logout', button_text='Log Out', extra_classes='homepage-btn') }}
</section>
{% endblock %}
+4 -1
View File
@@ -6,7 +6,10 @@
{% block content %}
{% from 'button.jinja' import button %}
<section id="hero" class="form">
<form action="" class="container" method="POST">
<form action="{{ url_for('login.login') }}" class="container" method="POST">
{% if error %}
<p class="form-error">{{ error }}</p>
{% endif %}
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
+13 -2
View File
@@ -6,7 +6,10 @@
{% block content %}
{% from 'button.jinja' import button %}
<section id="hero" class="form">
<form action="" class="container" method="POST">
<form action="{{ url_for('login.register') }}" class="container" method="POST">
{% if error %}
<p class="form-error">{{ error }}</p>
{% endif %}
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
@@ -15,6 +18,14 @@
<label for="email">Email:</label>
<input type="text" id="email" name="email" required>
</div>
<div class="form-group">
<label for="account_type">Account type:</label>
<select id="account_type" name="account_type" required>
<option value="private_user">Private User</option>
<option value="teacher">Teacher</option>
<option value="student">Student</option>
<option value="community_leader">Community Leader</option>
</select> </div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password">
@@ -24,7 +35,7 @@
<input type="password" id="password_confirm" name="password_confirm">
</div>
<button type="submit" class="submit-button">
Login
Register
</button>
</form>
</section>
+2 -2
View File
@@ -40,9 +40,9 @@
your group can get involved through educational programs, fundraising, and awareness campaigns.
</p>
<div class="homepage-buttons">
{{ button(url='/register-school', button_text='Register Your School', extra_classes='homepage-btn') }}
{{ button(url='/register', button_text='Register Your School/Community', extra_classes='homepage-btn') }}
<!-- need to assign bg colour -->
{{ button(url='/register-community', button_text='Join as a Community', extra_classes='homepage-btn') }}
{{ button(url='/register', button_text='Register as a teacher', extra_classes='homepage-btn') }}
</div>
</section>
{% endblock %}
+3 -1
View File
@@ -15,7 +15,9 @@
<a href="/species/portal">Add Species</a>
<a href="/species">View Species</a>
<a href="/species_search">Search Species</a>
<a href="/login">Login page</a>
<a href="/login">Login</a>
<a href="/register">Register new user</a>
<a href="/dashboard">Dashboard</a>
</div>
</div>
{# todo #}