]> littlesong.place Git - littlesongplace.git/commitdiff
Add password reset page
authorChris Fulljames <christianfulljames@gmail.com>
Sat, 2 May 2026 15:28:16 +0000 (11:28 -0400)
committerChris Fulljames <christianfulljames@gmail.com>
Sat, 2 May 2026 15:28:16 +0000 (11:28 -0400)
src/littlesongplace/auth.py
src/littlesongplace/mail.py
src/littlesongplace/templates/password-reset.html [new file with mode: 0644]

index 30acdc803155b31a11dc632149d5c8078ba41561..21b15afd3c1b53d986b963321dede4aec6aacb3a 100644 (file)
@@ -13,6 +13,19 @@ bp = Blueprint("auth", __name__)
 def signup_get():
     return render_template("signup.html")
 
+def _check_password(password, password_confirm):
+    error = False
+    if password != password_confirm:
+        flash_and_log("Passwords do not match", "error")
+        error = True
+    elif len(password) < 8:
+        flash_and_log("Password must be at least 8 characters", "error")
+        error = True
+    return error
+
+def _hash_password(password):
+    return bcrypt.hashpw(password.encode(), bcrypt.gensalt())
+
 @bp.post("/signup")
 def signup_post():
     username = request.form["username"]
@@ -30,12 +43,7 @@ def signup_post():
         flash_and_log("Username cannot be more than 30 characters", "error")
         error = True
 
-    elif password != password_confirm:
-        flash_and_log("Passwords do not match", "error")
-        error = True
-    elif len(password) < 8:
-        flash_and_log("Password must be at least 8 characters", "error")
-        error = True
+    error = error or _check_password(password, password_confirm)
 
     if db.query("select * from users where username = ?", [username], one=True):
         flash_and_log(f"Username '{username}' is already taken", "error")
@@ -45,7 +53,7 @@ def signup_post():
         current_app.logger.info("Failed signup attempt")
         return redirect(request.referrer)
 
-    password = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
+    password = _hash_password(password)
     timestamp = datetime.now(timezone.utc).isoformat()
 
     user_data = db.query(
@@ -92,6 +100,56 @@ def login_post():
 
     return render_template("login.html")
 
+@bp.get("/change-password")
+def password_reset():
+    return render_template("password-reset.html")
+
+@bp.post("/change-password")
+def password_reset_post():
+    error = False
+
+    username = request.form["username"]
+    old_password = request.form["old_password"]
+    password = request.form["password"]
+    password_confirm = request.form["password_confirm"]
+
+    # Get user from DB
+    user_data = db.query("select * from users where username = ?", [username], one=True)
+
+    # Check username
+    if not user_data:
+        flash("Invalid username/password", "error")
+        error = True
+        
+    # Check old password
+    elif not bcrypt.checkpw(old_password.encode(), user_data["password"]):
+        flash("Invalid username/password", "error")
+        error = True
+
+    # Check new password
+    error = error or _check_password(password, password_confirm)
+
+    # Reload page on error
+    if error:
+        current_app.logger.info(f"Failed password reset attempt for {username}")
+        return redirect(request.referrer)
+
+    # OK - Update password
+    password = _hash_password(password)
+    db.query("update users set password = ? where userid = ?", [password, user_data["userid"]])
+    db.commit()
+
+    flash("Password updated.  Please sign in to continue.", "success")
+    current_app.logger.info(f"Changed password for {username}")
+
+    # Force logout
+    if "username" in session:
+        session.pop("username")
+    if "userid" in session:
+        session.pop("userid")
+
+    return redirect("/login")
+
 
 @bp.get("/logout")
 def logout():
index 1061dd3f14185f6a04ad0c4b00b082d00503ef8f..c0474c5332d6dd32224fca4754d3f8f07677e158 100644 (file)
@@ -7,7 +7,11 @@ from email.utils import make_msgid
 from_addr = "mail@littlesong.place"
 
 def send(to_addrs, subject, body):
-    with open(datadir.get_email_pass_path(), "r") as epfile:
+    email_pass_path = datadir.get_email_pass_path()
+    if not email_pass_path.exists():
+        return
+
+    with open(email_pass_path, "r") as epfile:
         pw = epfile.read().strip()
 
     msg = EmailMessage()
diff --git a/src/littlesongplace/templates/password-reset.html b/src/littlesongplace/templates/password-reset.html
new file mode 100644 (file)
index 0000000..21d34ae
--- /dev/null
@@ -0,0 +1,42 @@
+{% extends "base.html" %}
+
+{% block title %}Change Password{% endblock %}
+
+{% block body %}
+
+<h1>change password</h1>
+
+<p>
+    Forgot your password? Please email cfull at
+    <a href="mailto:mail@littlesong.place">mail@littlesong.place</a> to reset
+    it.
+</p>
+
+<form method="post" action="/change-password">
+    <div class="signup-form">
+        <label for="username">Username</label><br>
+        <input type="text" name="username" maxlength="30" required></input>
+    </div>
+
+    <div class="signup-form">
+        <label for="old_password">Old Password</label><br>
+        <input type="password" name="old_password" maxlength="100" required></input>
+    </div>
+
+    <div class="signup-form">
+        <label for="password">New Password</label><br>
+        <input type="password" name="password" maxlength="100" required></input>
+    </div>
+
+    <div class="signup-form">
+        <label for="password_confirm">Confirm New Password</label><br>
+        <input type="password" name="password_confirm" maxlength="100" required></input>
+    </div>
+
+    <div class="signup-form">
+        <input type="submit" value="Change Password"></input>
+    </div>
+</form>
+
+{% endblock %}
+