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"))
|
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 ────────────────────────────────────────────────────────
|
# ── Portal UI pages ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
|
|||||||
@@ -45,6 +45,8 @@
|
|||||||
<div class="px-3 pb-2 mt-auto text-muted" style="font-size:.75rem;">
|
<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 }}
|
<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>
|
<a href="{{ url_for('logout') }}" class="text-muted">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
<button class="sidebar-collapse-btn" id="sidebarToggle" type="button" aria-label="Collapse sidebar">
|
<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