From b30a7a363890e8276c8a28e61ac2a202d224b273 Mon Sep 17 00:00:00 2001 From: Calum Mackervoy Date: Mon, 13 Jan 2025 15:39:27 +0100 Subject: [PATCH] Populate ExistingUserLogin email if the user has proven knowledge When the user proves knowledge of their email during the login flow, we store the email entered in the session so that we can pre-fill it for them on the subsequent password form. Not all flows leading to this view involve the user proving their email however, so if the key is not present in the session or it doesn't match the user's public id then it is ignored. --- itou/www/login/constants.py | 1 + itou/www/login/forms.py | 13 ++- itou/www/login/views.py | 14 +++ tests/www/login/__snapshots__/tests.ambr | 126 +++++++++++++++++++++++ tests/www/login/tests.py | 34 ++++++ tests/www/signup/test_job_seeker.py | 4 + 6 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 itou/www/login/constants.py diff --git a/itou/www/login/constants.py b/itou/www/login/constants.py new file mode 100644 index 0000000000..5831a0e56a --- /dev/null +++ b/itou/www/login/constants.py @@ -0,0 +1 @@ +ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY = "job_seeker_login_email" diff --git a/itou/www/login/forms.py b/itou/www/login/forms.py index 67d313948d..4295b259ca 100644 --- a/itou/www/login/forms.py +++ b/itou/www/login/forms.py @@ -10,6 +10,7 @@ from itou.openid_connect.errors import format_error_modal_content from itou.users.enums import IdentityProvider from itou.users.models import User +from itou.www.login.constants import ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY class FindExistingUserViaEmailForm(forms.Form): @@ -49,6 +50,7 @@ def clean_email(self): extra_tags="modal login_failure email_does_not_exist", ) raise ValidationError("Cette adresse e-mail est inconnue. Veuillez en saisir une autre, ou vous inscrire.") + self.request.session[ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY] = email return email @@ -57,15 +59,22 @@ class ItouLoginForm(LoginForm): demo_banner_account = forms.BooleanField(widget=forms.HiddenInput(), required=False) def __init__(self, *args, **kwargs): + user_email = kwargs.pop("user_email", None) super().__init__(*args, **kwargs) self.fields["password"].widget.attrs["placeholder"] = "**********" self.fields["password"].help_text = format_html( 'Mot de passe oublié ?', reverse("account_reset_password"), ) - self.fields["login"].widget.attrs["placeholder"] = "adresse@email.fr" self.fields["login"].label = "Adresse e-mail" - self.fields["login"].widget.attrs["autofocus"] = True + + if user_email: + self.fields["login"].initial = user_email + self.fields["login"].widget.attrs["disabled"] = True + self.data = self.data.dict() | {"login": user_email} + else: + self.fields["login"].widget.attrs["placeholder"] = "adresse@email.fr" + self.fields["login"].widget.attrs["autofocus"] = True def clean(self): # Parent method performs authentication on form success. diff --git a/itou/www/login/views.py b/itou/www/login/views.py index d680c1f7ec..e7c7acb614 100644 --- a/itou/www/login/views.py +++ b/itou/www/login/views.py @@ -13,6 +13,7 @@ from itou.users.models import User from itou.utils.auth import LoginNotRequiredMixin from itou.utils.urls import add_url_params, get_safe_url, get_url_param_value +from itou.www.login.constants import ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY from itou.www.login.forms import FindExistingUserViaEmailForm, ItouLoginForm @@ -180,6 +181,19 @@ def setup(self, request, *args, **kwargs): self.user = get_object_or_404(User, public_id=self.kwargs["user_public_id"]) self.user_kind = self.user.kind + def get_user_email(self): + """ + This template can optionally have the email of the user displayed, if it is safe to do so. + This is done using the session and comparing the stashed value to the user concerned by this view. + """ + stashed_email = self.request.session.get(ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY, None) + return stashed_email if stashed_email == self.user.email else None + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["user_email"] = self.get_user_email() + return kwargs + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) extra_context = { diff --git a/tests/www/login/__snapshots__/tests.ambr b/tests/www/login/__snapshots__/tests.ambr index 165530ca11..b8a7d579e7 100644 --- a/tests/www/login/__snapshots__/tests.ambr +++ b/tests/www/login/__snapshots__/tests.ambr @@ -236,6 +236,132 @@ + + ''' +# --- +# name: TestExistingUserLogin.test_login_email_prefilled[login_not_prefilled] + ''' +
+ + + + + +
+ +
+

Se connecter avec vos identifiants

+ + +
+
+ + +
+ +
+
+
+
+ + + +
+
+
+ * champs obligatoires +
+ + +
+ + + +
+
+
+
+ + + +
+ + +
+ ''' +# --- +# name: TestExistingUserLogin.test_login_email_prefilled[login_prefilled] + ''' +
+ + + + + +
+ +
+

Se connecter avec vos identifiants

+ + +
+
+ + +
+ +
+
+
+
+ + + +
+
+
+ * champs obligatoires +
+ + +
+ + + +
+
+
+
+ + + +
+ +
''' # --- diff --git a/tests/www/login/tests.py b/tests/www/login/tests.py index 572804610d..74ab7e1af3 100644 --- a/tests/www/login/tests.py +++ b/tests/www/login/tests.py @@ -15,6 +15,7 @@ from itou.users.enums import IdentityProvider, UserKind from itou.utils import constants as global_constants from itou.utils.urls import add_url_params +from itou.www.login.constants import ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY from itou.www.login.forms import ItouLoginForm from itou.www.login.views import ExistingUserLoginView from tests.openid_connect.france_connect.tests import FC_USERINFO, mock_oauth_dance @@ -225,6 +226,9 @@ def test_pre_login_redirects_to_existing_user(self, client): response = client.post(url, data=form_data) assertRedirects(response, f'{reverse("login:existing_user", args=(user.public_id,))}?back_url={url}') + # Email is populated in session. The utility of this is covered by the ExistingUserLoginView tests. + assert client.session[ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY] == user.email + def test_pre_login_email_unknown(self, client, snapshot): url = reverse("login:job_seeker") response = client.get(url) @@ -320,6 +324,36 @@ def test_login_django(self, client): response = client.post(url, data=form_data) assertRedirects(response, reverse("account_email_verification_sent")) + def test_login_email_prefilled(self, client, snapshot): + # Login is not pre-filled just by visiting the page. + # The user must prove they know this information + user = JobSeekerFactory(identity_provider=IdentityProvider.DJANGO, for_snapshot=True) + url = reverse("login:existing_user", args=(user.public_id,)) + response = client.get(url) + assert response.status_code == 200 + + assert response.context["form"]["login"].initial is None + assert str(parse_response_to_soup(response, selector=".c-form")) == snapshot(name="login_not_prefilled") + + # If the email has been populated in the session, but the email populated does not match the user requested, + # then it is ignored. + session = client.session + session[ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY] = "someoneelse@emaildomain.xyz" + session.save() + response = client.get(url) + assert response.status_code == 200 + assert response.context["form"]["login"].initial is None + assert str(parse_response_to_soup(response, selector=".c-form")) == snapshot(name="login_not_prefilled") + + # If the login has been populated in the session with the correct email, + # then the user will not need to re-enter it a second time. + session[ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY] = user.email + session.save() + response = client.get(url) + assert response.status_code == 200 + assert response.context["form"]["login"].initial == user.email + assert str(parse_response_to_soup(response, selector=".c-form")) == snapshot(name="login_prefilled") + @pytest.mark.parametrize( "identity_provider", [ diff --git a/tests/www/signup/test_job_seeker.py b/tests/www/signup/test_job_seeker.py index 1192db6bff..134fe11f87 100644 --- a/tests/www/signup/test_job_seeker.py +++ b/tests/www/signup/test_job_seeker.py @@ -15,6 +15,7 @@ from itou.users.models import User from itou.utils import constants as global_constants from itou.utils.widgets import DuetDatePickerWidget +from itou.www.login.constants import ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY from itou.www.signup.forms import JobSeekerSituationForm from tests.asp.factories import CountryEuropeFactory, CountryFranceFactory from tests.cities.factories import create_city_geispolsheim, create_test_cities @@ -618,6 +619,9 @@ def test_job_seeker_signup_with_conflicting_fields(self, erroneous_fields, snaps ) assertContains(response, reverse("login:existing_user", args=(existing_user.public_id,))) + # Important that the email is not saved in the session, in the subsequent step the user must prove they know it + assert ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY not in client.session + # NOTE: error is rendered for the case that the user ignores the modal if "email" in erroneous_fields: assert response.context["form"].errors["email"] == [