Repository: 3cn-ecn/nantralPlatform

Issue:
Mot de passe perdu
Cette fonctionnalité de fonctionne pas sur mon ordinateur mais fonctionne sur l'ordinateur de Gabin Schieffer <br/> Proposé par julie.geffraye@eleves.ec-nantes.fr


Discussion:
j'ai testé sur mon PC, la réinitialisation du mot de passe fonctionne.
sur certains ordinateur, cela donne une erreur 500 lors de l'envoi du formulaire de réinitialisation (y compris après suppression des cookies)

@unitrium t'est-il possible de consulter les logs serveur pour en savoir plus ?
Je vais essayer de te les obtenir mais j'avoue qu'on les stocke pas trop donc ce seront que les plus récents
```
Attaching to deployment_server_1
server_1    | ========= Migrate database
server_1    | /usr/local/lib/python3.7/site-packages/environ/environ.py:630: UserWarning: /var/app/config/settings/.env doesn't exist - if you're not configuring your environment separately, create one.
server_1    |   "environment separately, create one." % env_file)
server_1    | Operations to perform:
server_1    |   Apply all migrations: academic, admin, auth, contenttypes, event, group, sessions, student
server_1    | Running migrations:
server_1    |   No migrations to apply.
server_1    |   Your models have changes that are not yet reflected in a migration, and so won't be applied.
server_1    |   Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them.
server_1    | ========= DONE ============
server_1    | ========= Collect static files
server_1    | /usr/local/lib/python3.7/site-packages/environ/environ.py:630: UserWarning: /var/app/config/settings/.env doesn't exist - if you're not configuring your environment separately, create one.
server_1    |   "environment separately, create one." % env_file)
server_1    | 
server_1    | 0 static files copied to '/var/app/staticfiles', 169 unmodified.
server_1    | ========= DONE ============
server_1    | ========= Starting server =========
server_1    | [2020-10-06 21:44:06 +0000] [9] [INFO] Starting gunicorn 20.0.4
server_1    | [2020-10-06 21:44:06 +0000] [9] [INFO] Listening at: http://0.0.0.0:8000 (9)
server_1    | [2020-10-06 21:44:06 +0000] [9] [INFO] Using worker: sync
server_1    | [2020-10-06 21:44:06 +0000] [12] [INFO] Booting worker with pid: 12
server_1    | /usr/local/lib/python3.7/site-packages/environ/environ.py:630: UserWarning: /var/app/config/settings/.env doesn't exist - if you're not configuring your environment separately, create one.
server_1    |   "environment separately, create one." % env_file)
server_1    | pre_remove
server_1    | post_remove
server_1    | pre_add
server_1    | post_add
server_1    | pre_add
server_1    | post_add
server_1    | /usr/local/lib/python3.7/site-packages/storages/backends/s3boto3.py:341: UserWarning: The default behavior of S3Boto3Storage is insecure and will change in django-storages 1.10. By default files and new buckets are saved with an ACL of 'public-read' (globally publicly readable). Version 1.10 will default to using the bucket's ACL. To opt into the new behavior set AWS_DEFAULT_ACL = None, otherwise to silence this warning explicitly set AWS_DEFAULT_ACL.
server_1    |   "The default behavior of S3Boto3Storage is insecure and will change "
```
Malheuresement vu que le serveur est en DEBUG=TRUE le logging n'est pas automatique, donc il y a tres peu d'infos
J'essaye de voir si je peux obtenir plus
Enfait c'est les logs de quand le serveur a redémarré après l'erreur 500 donc normal qu'il y a pas
Le bug a l'air d'être présent partout désormais, on obtient une erreur 500 dès qu'on clique sur le lien "Mot de passe oublié"

Resolving patch:
diff --git a/server/apps/account/urls.py b/server/apps/account/urls.py
--- a/server/apps/account/urls.py
+++ b/server/apps/account/urls.py
@@ -22,7 +22,7 @@
     path('permanent', PermanentAccountUpgradeView.as_view(),
          name='upgrade-permanent'),
     path('forgotten', ForgottenPassView.as_view(), name='forgotten_pass'),
-    path('reset_pass/<slug:uidb64>/<slug:token>',
+    path('reset_pass/<slug:uidb64>/<slug:token>/',
          PasswordResetConfirmCustomView.as_view(), name='reset_pass'),
     path('<slug:user_id>/student', redirect_to_student, name='redirect-student'),
 ]
diff --git a/server/apps/account/views.py b/server/apps/account/views.py
--- a/server/apps/account/views.py
+++ b/server/apps/account/views.py
@@ -176,19 +176,22 @@ class ForgottenPassView(FormView):
     template_name = 'account/forgotten_pass.html'
 
     def form_valid(self, form):
-        user = User.objects.get(email=form.cleaned_data['email'])
-        if user is not None:
-            subject = '[Nantral Platform] Reinitialisation de votre mot de passe'
-            current_site = get_current_site(self.request)
-            message = render_to_string('account/mail/password_request.html', {
-                'user': user,
-                'domain': current_site.domain,
-                'uidb64': urlsafe_base64_encode(force_bytes(user.pk)),
-                # method will generate a hash value with user related data
-                'token': account_activation_token.make_token(user),
-            })
-            user.email_user(
-                subject, message, 'accounts@nantral-platform.fr', html_message=message)
+        try:
+            user = User.objects.get(email=form.cleaned_data['email'])
+            if user is not None:
+                subject = '[Nantral Platform] Reinitialisation de votre mot de passe'
+                current_site = get_current_site(self.request)
+                message = render_to_string('account/mail/password_request.html', {
+                    'user': user,
+                    'domain': current_site.domain,
+                    'uidb64': urlsafe_base64_encode(force_bytes(user.pk)),
+                    # method will generate a hash value with user related data
+                    'token': account_activation_token.make_token(user),
+                })
+                user.email_user(
+                    subject, message, 'accounts@nantral-platform.fr', html_message=message)
+        except User.DoesNotExist:
+            pass
         messages.success(
             self.request, 'Un email de récuperation a été envoyé si cette adresse existe.')
         return redirect(reverse('account:login'))


Test patch:
diff --git a/server/apps/account/tests.py b/server/apps/account/tests.py
--- a/server/apps/account/tests.py
+++ b/server/apps/account/tests.py
@@ -392,3 +392,71 @@ def test_temp_request_expired(self, mock_post):
             response = self.client.post(url, payload)
             self.assertEqual(response.status_code, 302)
             self.assertFalse(get_user(self.client).is_authenticated)
+
+
+class TestForgottenPass(TestCase, TestMixin):
+
+    def setUp(self) -> None:
+        self.create_user(username='test', email='test@ec-nantes.fr')
+
+    def tearDown(self) -> None:
+        self.user_teardown()
+
+    def test_forgotten_pass_request(self):
+        url = reverse('account:forgotten_pass')
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        payload = {
+            'email': 'not@ec-nantes.fr'
+        }
+        # Check that you cannot send email to unexisting email.
+        response = self.client.post(url, data=payload)
+        self.assertEqual(response.status_code, 302)
+        assert len(mail.outbox) == 0
+
+        payload = {
+            'email': 'test@ec-nantes.fr'
+        }
+        response = self.client.post(url, data=payload)
+        self.assertEqual(response.status_code, 302)
+        assert len(mail.outbox) == 1
+        extract = re.search(
+            "<a href='http:\/\/testserver/account/reset_pass/([^/]*)/([^/]*)", mail.outbox[0].body)
+        uidb64 = extract.group(1)
+        token = extract.group(2)
+        url = reverse('account:reset_pass', kwargs={
+                      'uidb64': uidb64, 'token': 'token'})
+
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'Lien invalide')
+        payload = {
+            'new_password1': 'new',
+            'new_password2': 'new'
+        }
+        response = self.client.post(url, payload)
+        self.assertEqual(response.status_code, 200)
+
+        # Check that you still cannot login
+        url = reverse('account:login')
+        payload = {
+            'email': 'test@ec-nantes.fr',
+            'password': 'new'
+        }
+        response = self.client.post(url, payload)
+        self.assertEqual(response.status_code, 302)
+        self.assertFalse(get_user(self.client).is_authenticated)
+
+        # Check with a valid token now
+
+        url = reverse('account:reset_pass', kwargs={
+                      'uidb64': uidb64, 'token': token})
+
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 302)
+        payload = {
+            'new_password1': 'new',
+            'new_password2': 'new'
+        }
+        response = self.client.post(url, payload)
+        self.assertEqual(response.status_code, 302)