Ajoute une page de profil (modifier ses infos, mot de passe, supprimer son compte)

Le nom affiché dans la barre de navigation (base.html) devient un lien
vers /profile : informations du compte (email en lecture seule, nom/
prénom modifiables), changement de mot de passe (mot de passe actuel
requis + mêmes règles de force qu'à l'inscription/la réinitialisation),
et une section "danger" pour demander la suppression du compte.

La suppression exige le mot de passe actuel ET la saisie exacte de
"SUPPRIMER" (deux confirmations distinctes pour une action irréversible)
— routes/auth/profile.py. Pour un compte "user" (limité à un seul projet,
créé automatiquement et impossible à supprimer autrement, voir
core/auth_guard.py), supprimer le compte supprime aussi son unique projet
sur le disque, faute de quoi il resterait orphelin sans plus aucun
propriétaire. Un compte "admin" peut posséder plusieurs jeux qui ne lui
sont pas dédiés de la même façon : ses projets ne sont jamais touchés.
L'unique compte administrateur ne peut pas être supprimé (auth/
count_admins.py) — le supprimer bloquerait la création d'un nouveau
compte admin (réservée au tout premier compte jamais créé, base vide).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-08-28 21:03:47 +02:00
co-authored by Claude Sonnet 5
parent d5a84413d1
commit 0d46abe193
9 changed files with 379 additions and 2 deletions
+143
View File
@@ -0,0 +1,143 @@
"""Page de profil (routes/auth/profile.py) : modifier nom/prénom, changer
de mot de passe, demander la suppression de son compte (avec mot de passe
+ confirmation textuelle, voir templates/auth/profile.html)."""
import pyotp
import auth
import db
from tests.test_auth import _register, _confirm_2fa, _cleanup_project, anon_client # noqa: F401
def test_profile_page_shows_current_user_info(anon_client):
_register(anon_client, "profileinfo@example.com", prenom="Ada", nom="Lovelace")
_confirm_2fa(anon_client)
try:
html = anon_client.get("/profile").get_data(as_text=True)
assert "profileinfo@example.com" in html
assert 'value="Ada"' in html
assert 'value="Lovelace"' in html
finally:
_cleanup_project("profileinfo@example.com")
def test_update_name(anon_client):
_register(anon_client, "updatename@example.com", prenom="Old", nom="Name")
_confirm_2fa(anon_client)
try:
resp = anon_client.post("/profile/update-name", data={"prenom": "New", "nom": "Name2"})
assert "mises à jour" in resp.get_data(as_text=True)
user = auth.get_user_by_email("updatename@example.com")
assert user["prenom"] == "New"
assert user["nom"] == "Name2"
finally:
_cleanup_project("updatename@example.com")
def test_update_name_requires_both_fields(anon_client):
_register(anon_client, "updatenameblank@example.com")
_confirm_2fa(anon_client)
try:
resp = anon_client.post("/profile/update-name", data={"prenom": "", "nom": "Test"})
assert "requis" in resp.get_data(as_text=True)
finally:
_cleanup_project("updatenameblank@example.com")
def test_change_password_requires_correct_current_password(anon_client):
_register(anon_client, "changepwwrong@example.com")
_confirm_2fa(anon_client)
try:
resp = anon_client.post("/profile/change-password", data={
"current_password": "wrong", "new_password": "Nouveau$ecret1", "new_password_confirm": "Nouveau$ecret1",
})
assert "incorrect" in resp.get_data(as_text=True)
finally:
_cleanup_project("changepwwrong@example.com")
def test_change_password_rejects_mismatched_confirmation(anon_client):
_register(anon_client, "changepwmismatch@example.com")
_confirm_2fa(anon_client)
try:
resp = anon_client.post("/profile/change-password", data={
"current_password": "Sup3r$ecret!", "new_password": "Nouveau$ecret1", "new_password_confirm": "Autre$ecret2",
})
assert "ne correspondent pas" in resp.get_data(as_text=True)
finally:
_cleanup_project("changepwmismatch@example.com")
def test_change_password_rejects_a_weak_new_password(anon_client):
_register(anon_client, "changepwweak@example.com")
_confirm_2fa(anon_client)
try:
resp = anon_client.post("/profile/change-password", data={
"current_password": "Sup3r$ecret!", "new_password": "faible", "new_password_confirm": "faible",
})
assert "faible" in resp.get_data(as_text=True)
finally:
_cleanup_project("changepwweak@example.com")
def test_change_password_success_lets_login_with_new_password(anon_client):
_register(anon_client, "changepwok@example.com")
_confirm_2fa(anon_client)
try:
resp = anon_client.post("/profile/change-password", data={
"current_password": "Sup3r$ecret!", "new_password": "Nouveau$ecret1", "new_password_confirm": "Nouveau$ecret1",
})
assert "modifié" in resp.get_data(as_text=True)
anon_client.post("/logout")
resp = anon_client.post("/login", data={"email": "changepwok@example.com", "password": "Nouveau$ecret1"}, follow_redirects=False)
assert resp.status_code == 302
assert resp.headers["Location"] == "/login/2fa"
finally:
_cleanup_project("changepwok@example.com")
def test_delete_account_requires_correct_password(anon_client):
_register(anon_client, "deletewrongpw@example.com")
_confirm_2fa(anon_client)
try:
resp = anon_client.post("/profile/delete", data={"current_password": "wrong", "confirm_text": "SUPPRIMER"})
assert "incorrect" in resp.get_data(as_text=True)
assert auth.get_user_by_email("deletewrongpw@example.com") is not None
finally:
_cleanup_project("deletewrongpw@example.com")
def test_delete_account_requires_the_confirmation_phrase(anon_client):
_register(anon_client, "deletenoconfirm@example.com")
_confirm_2fa(anon_client)
try:
resp = anon_client.post("/profile/delete", data={"current_password": "Sup3r$ecret!", "confirm_text": "oups"})
assert "annulée" in resp.get_data(as_text=True)
assert auth.get_user_by_email("deletenoconfirm@example.com") is not None
finally:
_cleanup_project("deletenoconfirm@example.com")
def test_delete_account_removes_user_and_their_project(anon_client):
_register(anon_client, "deleteforreal@example.com")
_confirm_2fa(anon_client)
user = auth.get_user_by_email("deleteforreal@example.com")
slug = user["project_slug"]
import os
assert os.path.isdir(db.game_dir(slug))
resp = anon_client.post("/profile/delete", data={"current_password": "Sup3r$ecret!", "confirm_text": "SUPPRIMER"}, follow_redirects=False)
assert resp.status_code == 302
assert resp.headers["Location"] == "/login"
assert auth.get_user_by_email("deleteforreal@example.com") is None
assert not os.path.isdir(db.game_dir(slug))
def test_the_only_admin_account_cannot_be_deleted(client):
"""`client` (conftest.py) est déjà connecté en tant qu'admin unique de
la session de tests."""
resp = client.post("/profile/delete", data={"current_password": "Test1234!", "confirm_text": "SUPPRIMER"})
assert "administrateur" in resp.get_data(as_text=True)
import tests.conftest as conftest_module
assert auth.get_user_by_id(conftest_module._TEST_ADMIN_ID) is not None