diff --git a/i18n.py b/i18n.py index 5150420..f04717e 100644 --- a/i18n.py +++ b/i18n.py @@ -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", + }, } diff --git a/meals.py b/meals.py index 88fed15..dc6d0cb 100644 --- a/meals.py +++ b/meals.py @@ -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/", methods=["POST"]) @login_required def admin_remove_user(user_id): diff --git a/models.py b/models.py index b50568d..9402a8a 100644 --- a/models.py +++ b/models.py @@ -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: diff --git a/templates/dashboard.html b/templates/dashboard.html index adef25a..56928db 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -85,15 +85,14 @@
- {{ t('⚙ Account Settings') }} + {{ t('👤 Account Settings') }}
-
-

{{ t('Delete My Account') }}

- - + +

🔑 {{ t('Change Password') }}

+ + + +
@@ -113,7 +112,22 @@
- {% if is_admin %} +
+

{{ t('Delete My Account') }}

+

{{ t('This will permanently delete your account and all your responses.') }}

+ + +
+
+ + + {% if is_admin %} +
+ {{ t('🏠 Household Settings') }} +

🔗 {{ t('Public Sharing Link') }}

{{ 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.') }}

@@ -150,9 +164,7 @@
- {% endif %} - {% if is_admin %}
- {% endif %}
+ {% endif %}