forked from Ugric/Komodo-Hub
Merge pull request 'added user accounts and dashboard' (#56) from TommyHEC/Komodo-Hub:main into main
Reviewed-on: Ugric/Komodo-Hub#56
This commit is contained in:
commit
45e4d63f2f
10 files changed
+230
-15
No files matched your search
Binary file not shown.
+44
-1
@@ -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):
|
||||
|
||||
@@ -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
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 #}
|
||||
|
||||
Reference in new issue
Block a user