feat: password change + split account/household settings UI
- New /change-password route: validates current password, enforces min 4 chars, requires confirmation match - update_password() helper in models.py - Dashboard UI split into two collapsible sections: 👤 Account Settings (password, ntfy, delete account) 🏠 Household Settings (admin only: public link, timezone, delete household) - 14 new i18n keys with fr/de translations - Restored accidentally dropped 'Delete My Account' key
This commit is contained in:
@@ -166,14 +166,6 @@ TRANSLATIONS: dict[str, dict[str, str]] = {
|
||||
"fr": "⚙ Paramètres du compte",
|
||||
"de": "⚙ Kontoeinstellungen",
|
||||
},
|
||||
"Delete My Account": {
|
||||
"fr": "Supprimer mon compte",
|
||||
"de": "Mein Konto löschen",
|
||||
},
|
||||
"Enter your password to confirm": {
|
||||
"fr": "Entrez votre mot de passe pour confirmer",
|
||||
"de": "Geben Sie Ihr Passwort zur Bestätigung ein",
|
||||
},
|
||||
"Delete Household": {
|
||||
"fr": "Supprimer le foyer",
|
||||
"de": "Haushalt löschen",
|
||||
@@ -466,6 +458,60 @@ TRANSLATIONS: dict[str, dict[str, str]] = {
|
||||
"fr": "Copié !",
|
||||
"de": "Kopiert!",
|
||||
},
|
||||
|
||||
# ── Password change ──
|
||||
"👤 Account Settings": {
|
||||
"fr": "👤 Paramètres du compte",
|
||||
"de": "👤 Kontoeinstellungen",
|
||||
},
|
||||
"🏠 Household Settings": {
|
||||
"fr": "🏠 Paramètres du foyer",
|
||||
"de": "🏠 Haushaltseinstellungen",
|
||||
},
|
||||
"Change Password": {
|
||||
"fr": "Changer le mot de passe",
|
||||
"de": "Passwort ändern",
|
||||
},
|
||||
"Current password": {
|
||||
"fr": "Mot de passe actuel",
|
||||
"de": "Aktuelles Passwort",
|
||||
},
|
||||
"New password": {
|
||||
"fr": "Nouveau mot de passe",
|
||||
"de": "Neues Passwort",
|
||||
},
|
||||
"Confirm new password": {
|
||||
"fr": "Confirmer le nouveau mot de passe",
|
||||
"de": "Neues Passwort bestätigen",
|
||||
},
|
||||
"Password changed successfully.": {
|
||||
"fr": "Mot de passe changé avec succès.",
|
||||
"de": "Passwort erfolgreich geändert.",
|
||||
},
|
||||
"Current password is incorrect.": {
|
||||
"fr": "Le mot de passe actuel est incorrect.",
|
||||
"de": "Das aktuelle Passwort ist falsch.",
|
||||
},
|
||||
"New password must be at least 4 characters.": {
|
||||
"fr": "Le nouveau mot de passe doit comporter au moins 4 caractères.",
|
||||
"de": "Das neue Passwort muss mindestens 4 Zeichen lang sein.",
|
||||
},
|
||||
"New passwords do not match.": {
|
||||
"fr": "Les nouveaux mots de passe ne correspondent pas.",
|
||||
"de": "Die neuen Passwörter stimmen nicht überein.",
|
||||
},
|
||||
"This will permanently delete your account and all your responses.": {
|
||||
"fr": "Cela supprimera définitivement votre compte et toutes vos réponses.",
|
||||
"de": "Dies löscht Ihr Konto und alle Ihre Antworten dauerhaft.",
|
||||
},
|
||||
"Delete My Account": {
|
||||
"fr": "Supprimer mon compte",
|
||||
"de": "Mein Konto löschen",
|
||||
},
|
||||
"Enter your password to confirm": {
|
||||
"fr": "Entrez votre mot de passe pour confirmer",
|
||||
"de": "Geben Sie Ihr Passwort zur Bestätigung ein",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from flask import (
|
||||
flash,
|
||||
)
|
||||
|
||||
from werkzeug.security import check_password_hash
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
from models import (
|
||||
get_dashboard_data,
|
||||
@@ -33,6 +33,7 @@ from models import (
|
||||
get_household_by_public_token,
|
||||
generate_public_token,
|
||||
set_public_enabled,
|
||||
update_password,
|
||||
)
|
||||
from i18n import t
|
||||
from ntfy import send_test_notification, send_meal_reminder
|
||||
@@ -191,6 +192,33 @@ def delete_account():
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
|
||||
@meals_bp.route("/change-password", methods=["POST"])
|
||||
@login_required
|
||||
def change_password():
|
||||
"""Change the logged-in user's password."""
|
||||
current = request.form.get("current_password", "")
|
||||
new = request.form.get("new_password", "")
|
||||
confirm = request.form.get("confirm_password", "")
|
||||
|
||||
user = get_user_by_id(session["user_id"])
|
||||
|
||||
if not check_password_hash(user["password_hash"], current):
|
||||
flash(t("Current password is incorrect."), "error")
|
||||
return redirect(url_for("meals.dashboard"))
|
||||
|
||||
if len(new) < 4:
|
||||
flash(t("New password must be at least 4 characters."), "error")
|
||||
return redirect(url_for("meals.dashboard"))
|
||||
|
||||
if new != confirm:
|
||||
flash(t("New passwords do not match."), "error")
|
||||
return redirect(url_for("meals.dashboard"))
|
||||
|
||||
update_password(session["user_id"], generate_password_hash(new))
|
||||
flash(t("Password changed successfully."), "success")
|
||||
return redirect(url_for("meals.dashboard"))
|
||||
|
||||
|
||||
@meals_bp.route("/admin/remove-user/<int:user_id>", methods=["POST"])
|
||||
@login_required
|
||||
def admin_remove_user(user_id):
|
||||
|
||||
@@ -195,6 +195,13 @@ def set_public_enabled(household_id: int, enabled: bool) -> None:
|
||||
|
||||
# ── Users ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
def update_password(user_id: int, new_password_hash: str) -> None:
|
||||
"""Update a user's password hash."""
|
||||
with get_db() as db:
|
||||
db.execute("UPDATE users SET password_hash = ? WHERE id = ?",
|
||||
(new_password_hash, user_id))
|
||||
|
||||
|
||||
def get_user_by_id(user_id: int) -> dict | None:
|
||||
"""Fetch a single user by ID."""
|
||||
with get_db() as db:
|
||||
|
||||
+23
-11
@@ -85,15 +85,14 @@
|
||||
|
||||
<div class="manage-section">
|
||||
<details class="manage-details">
|
||||
<summary>{{ t('⚙ Account Settings') }}</summary>
|
||||
<summary>{{ t('👤 Account Settings') }}</summary>
|
||||
<div class="manage-forms">
|
||||
<form method="POST" action="{{ url_for('meals.delete_account') }}"
|
||||
onsubmit="return confirm(this.dataset.confirmMsg);"
|
||||
data-confirm-msg="{{ t('Delete YOUR account? This cannot be undone.') }}"
|
||||
class="manage-form">
|
||||
<h4>{{ t('Delete My Account') }}</h4>
|
||||
<input type="password" name="password" placeholder="{{ t('Enter your password to confirm') }}" required>
|
||||
<button class="btn btn-danger">{{ t('Delete My Account') }}</button>
|
||||
<form method="POST" action="{{ url_for('meals.change_password') }}" class="manage-form">
|
||||
<h4>🔑 {{ t('Change Password') }}</h4>
|
||||
<input type="password" name="current_password" placeholder="{{ t('Current password') }}" required>
|
||||
<input type="password" name="new_password" placeholder="{{ t('New password') }}" required style="margin-top: 8px;">
|
||||
<input type="password" name="confirm_password" placeholder="{{ t('Confirm new password') }}" required style="margin-top: 8px;">
|
||||
<button type="submit" class="btn btn-primary btn-sm" style="margin-top: 8px;">{{ t('Change Password') }}</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ url_for('meals.ntfy_settings') }}" class="manage-form">
|
||||
@@ -113,7 +112,22 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ url_for('meals.delete_account') }}"
|
||||
onsubmit="return confirm(this.dataset.confirmMsg);"
|
||||
data-confirm-msg="{{ t('Delete YOUR account? This cannot be undone.') }}"
|
||||
class="manage-form">
|
||||
<h4>{{ t('Delete My Account') }}</h4>
|
||||
<p class="hint">{{ t('This will permanently delete your account and all your responses.') }}</p>
|
||||
<input type="password" name="password" placeholder="{{ t('Enter your password to confirm') }}" required>
|
||||
<button class="btn btn-danger">{{ t('Delete My Account') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{% if is_admin %}
|
||||
<details class="manage-details" style="margin-top: 8px;">
|
||||
<summary>{{ t('🏠 Household Settings') }}</summary>
|
||||
<div class="manage-forms">
|
||||
<form method="POST" action="{{ url_for('meals.admin_public_link') }}" class="manage-form">
|
||||
<h4>🔗 {{ t('Public Sharing Link') }}</h4>
|
||||
<p class="hint">{{ t('Share a read-only view of your household\'s meal status. The link is hard to guess — only people with it can see the data.') }}</p>
|
||||
@@ -150,9 +164,7 @@
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary btn-sm" style="margin-top: 8px;">{{ t('Save') }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if is_admin %}
|
||||
<form method="POST" action="{{ url_for('meals.admin_delete_household') }}"
|
||||
onsubmit="return confirm(this.dataset.confirmMsg);"
|
||||
data-confirm-msg="{{ t('Delete the ENTIRE household and ALL members? This cannot be undone.') }}"
|
||||
@@ -162,9 +174,9 @@
|
||||
<input type="password" name="password" placeholder="{{ t('Enter your password to confirm') }}" required>
|
||||
<button class="btn btn-danger">{{ t('Delete Household') }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
Reference in New Issue
Block a user