Phase 24: add password change page

- GET/POST /account/password with CSRF token and bcrypt re-hash
- 'Password' link in sidebar next to Logout
- change_password.html template

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Mathieu Moran
2026-08-10 04:22:28 -04:00
co-authored by Copilot
parent c4dfcbaf25
commit 64b9481ce0
3 changed files with 90 additions and 0 deletions
+39
View File
@@ -354,6 +354,45 @@ def logout():
return redirect(url_for("login_page"))
@app.get("/account/password")
@require_login
def change_password_page():
csrf_token = secrets.token_hex(16)
session["csrf_token"] = csrf_token
return render_template("change_password.html", csrf_token=csrf_token)
@app.post("/account/password")
@require_login
def change_password():
token = request.form.get("csrf_token", "")
if not token or token != session.pop("csrf_token", None):
flash("Invalid request. Please try again.", "error")
return redirect(url_for("change_password_page"))
current = request.form.get("current_password", "")
new_pw = request.form.get("new_password", "")
confirm = request.form.get("confirm_password", "")
user_row = db.get_user_by_id(current_user.id)
if not user_row or not check_password(current, user_row["password_hash"]):
flash("Current password is incorrect.", "error")
return redirect(url_for("change_password_page"))
if len(new_pw) < 8:
flash("New password must be at least 8 characters.", "error")
return redirect(url_for("change_password_page"))
if new_pw != confirm:
flash("Passwords do not match.", "error")
return redirect(url_for("change_password_page"))
from modules.user_auth import hash_password
with db.get_cursor(commit=True) as cur:
cur.execute("UPDATE users SET password_hash = %s WHERE id = %s",
(hash_password(new_pw), current_user.id))
flash("Password updated successfully.", "success")
return redirect(url_for("change_password_page"))
# ── Portal UI pages ────────────────────────────────────────────────────────
@app.get("/")