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:
@@ -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("/")
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
<div class="px-3 pb-2 mt-auto text-muted" style="font-size:.75rem;">
|
||||
<i class="fas fa-user fa-fw"></i> {{ current_user.username }}
|
||||
·
|
||||
<a href="{{ url_for('change_password_page') }}" class="text-muted">Password</a>
|
||||
·
|
||||
<a href="{{ url_for('logout') }}" class="text-muted">Logout</a>
|
||||
</div>
|
||||
<button class="sidebar-collapse-btn" id="sidebarToggle" type="button" aria-label="Collapse sidebar">
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Change password – Yaris OBD Remote{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3 class="text-dark mb-3 fw-bold">Change password</h3>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'success' if category == 'success' else 'danger' }} py-2 small" role="alert">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="post" action="{{ url_for('change_password') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="mb-3">
|
||||
<label for="current_password" class="form-label small fw-semibold">Current password</label>
|
||||
<input type="password" class="form-control" id="current_password" name="current_password"
|
||||
autocomplete="current-password" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_password" class="form-label small fw-semibold">New password</label>
|
||||
<input type="password" class="form-control" id="new_password" name="new_password"
|
||||
autocomplete="new-password" required minlength="8">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirm_password" class="form-label small fw-semibold">Confirm new password</label>
|
||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password"
|
||||
autocomplete="new-password" required minlength="8">
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-key me-1"></i> Update password
|
||||
</button>
|
||||
<a href="{{ url_for('portal_index') }}" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user