Repository: zestedesavoir/zds-site

Issue:
 Profil et sécurité: rajouter un champ « Mot de passe »
Depuis ZdS:

> Bonsoir,
> 
> J'ai remarqué qu'il n'y avait pas de validation quand l'adresse email est modifié. Cela signifie que si quelqu'un passe sur l'ordinateur et modifie l'email, il pourra par la suite changer le mot de passe et prendre le contrôle du compte.
> 
> Aussi, ne serait-il pas judicieux de rajouter un champ de texte à remplir sur la page de la désinscription afin de prévenir les potentiels erreurs ?
Source: [Helmasaur](https://zestedesavoir.com/forums/sujet/12693/profil-et-securite/?page=1#p205342)

La correction semble simplement d'ajouter un champ « Mot de passe » sur les pages « Pseudo et courriel » et « Désinscription ».



Discussion:
Si quelqu'un souhaite s'occuper de ce ticket, il est possible de s'inspirer de la PR qui a été fermée https://github.com/zestedesavoir/zds-site/pull/5373
Vous pouvez vous inspirer de https://github.com/mattrobenolt/django-sudo

Resolving patch:
diff --git a/zds/member/forms.py b/zds/member/forms.py
--- a/zds/member/forms.py
+++ b/zds/member/forms.py
@@ -22,7 +22,7 @@
     validate_zds_password,
     validate_raw_zds_username,
 )
-from zds.utils.forms import IncludeEasyMDE
+from zds.utils.forms import IncludeEasyMDE, PasswordRequiredForm
 from zds.utils.misc import contains_utf8mb4
 from zds.utils.models import Licence, HatRequest, Hat
 from zds.utils import get_current_user
@@ -366,7 +366,7 @@ def __init__(self, *args, **kwargs):
         )
 
 
-class ChangeUserForm(forms.Form):
+class ChangeUserForm(PasswordRequiredForm):
     """
     Update username and email
     """
@@ -402,6 +402,8 @@ def __init__(self, user, *args, **kwargs):
         self.previous_username = user.username
         self.fields["options"].initial = ""
 
+        self.user = user
+
         if user.profile and user.profile.show_email:
             self.fields["options"].initial += "show_email"
 
@@ -409,6 +411,7 @@ def __init__(self, user, *args, **kwargs):
             Field("username", value=user.username),
             Field("email", value=user.email),
             Field("options"),
+            Field("password"),
             ButtonHolder(
                 StrictButton(_("Enregistrer"), type="submit"),
             ),
@@ -430,13 +433,40 @@ def clean(self):
         return cleaned_data
 
 
-# TODO: Updates the password --> requires a better name
-class ChangePasswordForm(forms.Form):
+class UnregisterForm(PasswordRequiredForm):
+    """
+    Unregister form
+    """
 
-    password_old = forms.CharField(
-        label=_("Mot de passe actuel"),
-        widget=forms.PasswordInput,
-    )
+    def __init__(self, user, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.helper = FormHelper()
+        self.helper.form_id = "unregister"
+        self.helper.form_class = "modal modal-flex"
+        self.helper.form_method = "post"
+        self.helper.form_action = reverse("member-unregister")
+
+        self.user = user
+
+        self.helper.layout = Layout(
+            Field("password"),
+            HTML(
+                _(
+                    """
+                <p>
+                    C’est votre dernière chance de rester parmi nous ...
+                </p>
+            """
+                )
+            ),
+            ButtonHolder(
+                StrictButton(_("Me désinscrire"), type="submit"),
+            ),
+        )
+
+
+# TODO: Updates the password --> requires a better name
+class ChangePasswordForm(PasswordRequiredForm):
 
     password_new = forms.CharField(
         label=_("Nouveau mot de passe"),
@@ -461,7 +491,7 @@ def __init__(self, user, *args, **kwargs):
         self.user = user
 
         self.helper.layout = Layout(
-            Field("password_old"),
+            Field("password"),
             Field("password_new"),
             Field("password_confirm"),
             ButtonHolder(
@@ -471,18 +501,6 @@ def __init__(self, user, *args, **kwargs):
 
     def clean(self):
         cleaned_data = super().clean()
-
-        password_old = cleaned_data.get("password_old")
-
-        # Check if the actual password is not empty
-        if password_old:
-            user_exist = authenticate(username=self.user.username, password=password_old)
-            # Check if the user exist with old informations.
-            if not user_exist and password_old != "":
-                self._errors["password_old"] = self.error_class([_("Mot de passe incorrect.")])
-                if "password_old" in cleaned_data:
-                    del cleaned_data["password_old"]
-
         return validate_passwords(cleaned_data, password_label="password_new", username=self.user.username)
 
 
diff --git a/zds/member/views/register.py b/zds/member/views/register.py
--- a/zds/member/views/register.py
+++ b/zds/member/views/register.py
@@ -24,7 +24,7 @@
     ProfileCreate,
     TokenGenerator,
 )
-from zds.member.forms import RegisterForm, UsernameAndEmailForm, LoginForm
+from zds.member.forms import RegisterForm, UsernameAndEmailForm, LoginForm, UnregisterForm
 from zds.member.models import (
     Profile,
     TokenRegister,
@@ -158,7 +158,10 @@ def warning_unregister(request):
     Display a warning page showing what will happen when the user
     unregisters.
     """
-    return render(request, "member/settings/unregister.html", {"user": request.user})
+    unregister_form = UnregisterForm(request.user)
+    return render(
+        request, "member/settings/unregister.html", {"user": request.user, "unregister_form": unregister_form}
+    )
 
 
 @login_required
@@ -167,6 +170,16 @@ def warning_unregister(request):
 def unregister(request):
     """Allow members to unregister."""
 
+    unregister_form = UnregisterForm(request.user, data=request.POST)
+
+    if not unregister_form.is_valid():
+        for field, errors in unregister_form.errors.items():
+            for error in errors:
+                messages.error(request, error)
+        return render(
+            request, "member/settings/unregister.html", {"user": request.user, "unregister_form": unregister_form}
+        )
+
     anonymous = get_object_or_404(User, username=settings.ZDS_APP["member"]["anonymous_account"])
     external = get_object_or_404(User, username=settings.ZDS_APP["member"]["external_account"])
     current = request.user
diff --git a/zds/utils/forms.py b/zds/utils/forms.py
--- a/zds/utils/forms.py
+++ b/zds/utils/forms.py
@@ -2,6 +2,8 @@
 
 from crispy_forms.bootstrap import StrictButton
 from crispy_forms.layout import Layout, ButtonHolder, Field, Div, HTML
+from django.contrib.auth import authenticate
+from django import forms
 from django.utils.translation import gettext_lazy as _
 from django.template import defaultfilters
 from zds.utils.models import Tag
@@ -157,3 +159,24 @@ def get_non_empty_field_or_error(self, cleaned_data, key, error_message):
     def check_text_length_limit(self, text, max_length, message_format):
         if len(text) > max_length:
             self._errors["text"] = [message_format().format(max_length)]
+
+
+class PasswordRequiredForm(forms.Form):
+
+    password = forms.CharField(
+        label=_("Mot de passe actuel"),
+        widget=forms.PasswordInput,
+    )
+
+    def clean(self):
+        cleaned_data = super().clean()
+        password = cleaned_data.get("password")
+
+        if password and self.user:
+            user_exist = authenticate(username=self.user.username, password=password)
+            # Check if the user exist.
+            if not user_exist and password != "":
+                self._errors["password"] = self.error_class([_("Mot de passe incorrect.")])
+                if "password" in cleaned_data:
+                    del cleaned_data["password"]
+        return cleaned_data


Test patch:
diff --git a/zds/member/tests/tests_forms.py b/zds/member/tests/tests_forms.py
--- a/zds/member/tests/tests_forms.py
+++ b/zds/member/tests/tests_forms.py
@@ -10,6 +10,7 @@
     NewPasswordForm,
     KarmaForm,
     UsernameAndEmailForm,
+    UnregisterForm,
 )
 from zds.member.models import BannedEmailProvider
 from zds.member.tests.factories import StaffProfileFactory
@@ -163,6 +164,21 @@ def test_username_slash_register_form(self):
         self.assertFalse(form.is_valid())
 
 
+class UnregisterFormTest(TestCase):
+    """
+    Check the unregistering form.
+    """
+
+    def setUp(self):
+        self.user1 = ProfileFactory()
+
+    def test_password_is_required(self):
+        data = {}
+        form = UnregisterForm(data=data, user=self.user1.user)
+        self.assertFalse(form.is_valid())
+        self.assertIn("password", form.errors.keys())
+
+
 class MiniProfileFormTest(TestCase):
     """
     Check the miniprofile form.
@@ -235,22 +251,22 @@ def setUp(self):
         self.user2 = ProfileFactory()
 
     def test_valid_change_username_user_form(self):
-        data = {"username": "MyNewPseudo", "email": self.user1.user.email}
+        data = {"username": "MyNewPseudo", "email": self.user1.user.email, "password": "hostel77"}
         form = ChangeUserForm(data=data, user=self.user1.user)
         self.assertTrue(form.is_valid())
 
     def test_valid_change_email_user_form(self):
-        data = {"username": self.user1.user.username, "email": "test@gmail.com"}
+        data = {"username": self.user1.user.username, "email": "test@gmail.com", "password": "hostel77"}
         form = ChangeUserForm(data=data, user=self.user1.user)
         self.assertTrue(form.is_valid())
 
     def test_already_used_username_user_form(self):
-        data = {"username": self.user2.user.username, "email": self.user1.user.email}
+        data = {"username": self.user2.user.username, "email": self.user1.user.email, "password": "hostel77"}
         form = ChangeUserForm(data=data, user=self.user1.user)
         self.assertFalse(form.is_valid())
 
     def test_already_used_email_user_form(self):
-        data = {"username": self.user1.user.username, "email": self.user2.user.email}
+        data = {"username": self.user1.user.username, "email": self.user2.user.email, "password": "hostel77"}
         form = ChangeUserForm(data=data, user=self.user1.user)
         self.assertFalse(form.is_valid())
 
@@ -258,28 +274,28 @@ def test_forbidden_email_provider_user_form(self):
         moderator = StaffProfileFactory().user
         if not BannedEmailProvider.objects.filter(provider="yopmail.com").exists():
             BannedEmailProvider.objects.create(provider="yopmail.com", moderator=moderator)
-        data = {"username": self.user1.user.username, "email": "test@yopmail.com"}
+        data = {"username": self.user1.user.username, "email": "test@yopmail.com", "password": "hostel77"}
         form = ChangeUserForm(data=data, user=self.user1.user)
         self.assertFalse(form.is_valid())
 
     def test_wrong_email_user_form(self):
-        data = {"username": self.user1.user.username, "email": "wrong@"}
+        data = {"username": self.user1.user.username, "email": "wrong@", "password": "hostel77"}
         form = ChangeUserForm(data=data, user=self.user1.user)
         self.assertFalse(form.is_valid())
 
-        data = {"username": self.user1.user.username, "email": "@test.com"}
+        data = {"username": self.user1.user.username, "email": "@test.com", "password": "hostel77"}
         form = ChangeUserForm(data=data, user=self.user1.user)
         self.assertFalse(form.is_valid())
 
-        data = {"username": self.user1.user.username, "email": "wrong@test"}
+        data = {"username": self.user1.user.username, "email": "wrong@test", "password": "hostel77"}
         form = ChangeUserForm(data=data, user=self.user1.user)
         self.assertFalse(form.is_valid())
 
-        data = {"username": self.user1.user.username, "email": "wrong@.com"}
+        data = {"username": self.user1.user.username, "email": "wrong@.com", "password": "hostel77"}
         form = ChangeUserForm(data=data, user=self.user1.user)
         self.assertFalse(form.is_valid())
 
-        data = {"username": self.user1.user.username, "email": "wrongtest.com"}
+        data = {"username": self.user1.user.username, "email": "wrongtest.com", "password": "hostel77"}
         form = ChangeUserForm(data=data, user=self.user1.user)
         self.assertFalse(form.is_valid())
 
@@ -287,6 +303,7 @@ def test_username_spaces_register_form(self):
         ProfileFactory()
         data = {
             "username": "  ZeTester  ",
+            "password": "hostel77",
             "email": self.user1.user.email,
         }
         form = ChangeUserForm(data=data, user=self.user1.user)
@@ -296,6 +313,7 @@ def test_username_coma_register_form(self):
         ProfileFactory()
         data = {
             "username": "Ze,Tester",
+            "password": "hostel77",
             "email": self.user1.user.email,
         }
         form = ChangeUserForm(data=data, user=self.user1.user)
@@ -305,10 +323,21 @@ def test_username_slash_changeuser_form(self):
         ProfileFactory()
         data = {
             "username": "Ze/Tester",
+            "password": "hostel77",
+            "email": self.user1.user.email,
+        }
+        form = ChangeUserForm(data=data, user=self.user1.user)
+        self.assertFalse(form.is_valid())
+
+    def test_password_is_required(self):
+        ProfileFactory()
+        data = {
+            "username": "ZeTester",
             "email": self.user1.user.email,
         }
         form = ChangeUserForm(data=data, user=self.user1.user)
         self.assertFalse(form.is_valid())
+        self.assertIn("password", form.errors.keys())
 
 
 class ChangePasswordFormTest(TestCase):
@@ -323,7 +352,7 @@ def setUp(self):
 
     def test_valid_change_password_form(self):
         data = {
-            "password_old": self.old_password,
+            "password": self.old_password,
             "password_new": self.new_password,
             "password_confirm": self.new_password,
         }
@@ -331,25 +360,25 @@ def test_valid_change_password_form(self):
         self.assertTrue(form.is_valid())
 
     def test_old_wrong_change_password_form(self):
-        data = {"password_old": "Wronnnng", "password_new": self.new_password, "password_confirm": self.new_password}
+        data = {"password": "Wronnnng", "password_new": self.new_password, "password_confirm": self.new_password}
         form = ChangePasswordForm(data=data, user=self.user1.user)
         self.assertFalse(form.is_valid())
 
     def test_not_matching_change_password_form(self):
-        data = {"password_old": self.old_password, "password_new": self.new_password, "password_confirm": "Wronnnng"}
+        data = {"password": self.old_password, "password_new": self.new_password, "password_confirm": "Wronnnng"}
         form = ChangePasswordForm(data=data, user=self.user1.user)
         self.assertFalse(form.is_valid())
 
     def test_too_short_change_password_form(self):
         too_short = "short"
-        data = {"password_old": self.old_password, "password_new": too_short, "password_confirm": too_short}
+        data = {"password": self.old_password, "password_new": too_short, "password_confirm": too_short}
         form = ChangePasswordForm(data=data, user=self.user1.user)
         self.assertFalse(form.is_valid())
 
     def test_match_username_change_password_form(self):
         self.user1.user.username = "LongName"
         data = {
-            "password_old": self.old_password,
+            "password": self.old_password,
             "password_new": self.user1.user.username,
             "password_confirm": self.user1.user.username,
         }
diff --git a/zds/member/tests/views/tests_emailproviders.py b/zds/member/tests/views/tests_emailproviders.py
--- a/zds/member/tests/views/tests_emailproviders.py
+++ b/zds/member/tests/views/tests_emailproviders.py
@@ -29,7 +29,7 @@ def test_new_provider_with_email_edit(self):
         # Edit the email with an unknown provider
         self.client.post(
             reverse("update-username-email-member"),
-            {"username": user.username, "email": "test@unknown-provider-edit.com"},
+            {"username": user.username, "email": "test@unknown-provider-edit.com", "password": "hostel77"},
             follow=False,
         )
         # A new provider object should have been created
diff --git a/zds/member/tests/views/tests_moderation.py b/zds/member/tests/views/tests_moderation.py
--- a/zds/member/tests/views/tests_moderation.py
+++ b/zds/member/tests/views/tests_moderation.py
@@ -438,7 +438,7 @@ def test_karma_and_pseudo_change(self):
         tester = ProfileFactory()
         old_pseudo = tester.user.username
         self.client.force_login(tester.user)
-        data = {"username": "dummy", "email": tester.user.email}
+        data = {"username": "dummy", "email": tester.user.email, "password": "hostel77"}
         result = self.client.post(reverse("update-username-email-member"), data, follow=False)
 
         self.assertEqual(result.status_code, 302)
diff --git a/zds/member/tests/views/tests_register.py b/zds/member/tests/views/tests_register.py
--- a/zds/member/tests/views/tests_register.py
+++ b/zds/member/tests/views/tests_register.py
@@ -87,13 +87,13 @@ def test_unregister(self):
 
         # test not logged user can't unregister.
         self.client.logout()
-        result = self.client.post(reverse("member-unregister"), follow=False)
+        result = self.client.post(reverse("member-unregister"), {"password": "hostel77"}, follow=False)
         self.assertEqual(result.status_code, 302)
 
         # test logged user can unregister.
         user = ProfileFactory()
         self.client.force_login(user.user)
-        result = self.client.post(reverse("member-unregister"), follow=False)
+        result = self.client.post(reverse("member-unregister"), {"password": "hostel77"}, follow=False)
         self.assertEqual(result.status_code, 302)
         self.assertEqual(User.objects.filter(username=user.user.username).count(), 0)
 
@@ -190,7 +190,7 @@ def test_unregister(self):
 
         # login and unregister:
         self.client.force_login(user.user)
-        result = self.client.post(reverse("member-unregister"), follow=False)
+        result = self.client.post(reverse("member-unregister"), {"password": "hostel77"}, follow=False)
         self.assertEqual(result.status_code, 302)
 
         # check that the bot have taken authorship of tutorial:
diff --git a/zds/tutorialv2/tests/tests_opinion_views.py b/zds/tutorialv2/tests/tests_opinion_views.py
--- a/zds/tutorialv2/tests/tests_opinion_views.py
+++ b/zds/tutorialv2/tests/tests_opinion_views.py
@@ -372,7 +372,7 @@ def test_opinion_unpublication(self, opinions_management):
 
         # unregister author and unpublish
         self.client.force_login(self.user_author)
-        result = self.client.post(reverse("member-unregister"), follow=False)
+        result = self.client.post(reverse("member-unregister"), {"password": "hostel77"}, follow=False)
         self.assertEqual(result.status_code, 302)
 
         self.client.force_login(self.user_staff)
@@ -598,7 +598,7 @@ def test_permanently_unpublish_opinion(self):
                 self.assertEqual(result.status_code, 302)
 
                 if unregister_author:
-                    result = self.client.post(reverse("member-unregister"), follow=False)
+                    result = self.client.post(reverse("member-unregister"), {"password": "hostel77"}, follow=False)
                     self.assertEqual(result.status_code, 302)
 
                 # login as staff
diff --git a/zds/tutorialv2/tests/tests_views/tests_events.py b/zds/tutorialv2/tests/tests_views/tests_events.py
--- a/zds/tutorialv2/tests/tests_views/tests_events.py
+++ b/zds/tutorialv2/tests/tests_views/tests_events.py
@@ -93,7 +93,7 @@ def test_events_involving_unregistered_users(self):
         # Unregister users
         for user in [self.author, self.coauthor, self.contributor]:
             self.client.force_login(user)
-            response = self.client.post(reverse("member-unregister"), follow=False)
+            response = self.client.post(reverse("member-unregister"), {"password": "hostel77"}, follow=False)
             self.assertEqual(response.status_code, 302)
 
         # Access the event list page
diff --git a/zds/tutorialv2/tests/tests_views/tests_published.py b/zds/tutorialv2/tests/tests_views/tests_published.py
--- a/zds/tutorialv2/tests/tests_views/tests_published.py
+++ b/zds/tutorialv2/tests/tests_views/tests_published.py
@@ -1384,7 +1384,7 @@ def test_unpublish_unregistered_author(self):
         registered_validation.save()
 
         self.client.force_login(self.user_author)
-        result = self.client.post(reverse("member-unregister"), follow=False)
+        result = self.client.post(reverse("member-unregister"), {"password": "hostel77"}, follow=False)
         self.assertEqual(result.status_code, 302)
 
         self.client.force_login(self.user_staff)