feat: send email to warn user about password change
This commit is contained in:
+35
-10
@@ -19,6 +19,21 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
INPUT_CLASSES = 'input input-bordered w-full'
|
INPUT_CLASSES = 'input input-bordered w-full'
|
||||||
|
|
||||||
|
|
||||||
|
def _send_account_notice(subject_template, text_template, html_template, context, to_email):
|
||||||
|
# Compartido por los avisos de seguridad de la cuenta (cambio de email,
|
||||||
|
# cambio de contraseña): mismo esqueleto de email (texto plano +
|
||||||
|
# alternativa HTML de crochet/email/base.html), solo cambian las
|
||||||
|
# plantillas y el destinatario.
|
||||||
|
subject = ''.join(render_to_string(subject_template, context).splitlines())
|
||||||
|
send_mail(
|
||||||
|
subject=subject,
|
||||||
|
message=render_to_string(text_template, context),
|
||||||
|
from_email=None,
|
||||||
|
recipient_list=[to_email],
|
||||||
|
html_message=render_to_string(html_template, context),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class StyledAuthenticationForm(AuthenticationForm):
|
class StyledAuthenticationForm(AuthenticationForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -47,6 +62,21 @@ class StyledPasswordChangeForm(PasswordChangeForm):
|
|||||||
for field in self.fields.values():
|
for field in self.fields.values():
|
||||||
field.widget.attrs['class'] = INPUT_CLASSES
|
field.widget.attrs['class'] = INPUT_CLASSES
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
user = super().save(commit=commit)
|
||||||
|
# A diferencia del cambio de email, aquí no hay una dirección
|
||||||
|
# "anterior" distinta: el aviso va al propio email de la cuenta.
|
||||||
|
# Sin email no hay a quién avisar (cuentas antiguas sin uno).
|
||||||
|
if user.email:
|
||||||
|
context = {'user': user, 'site_name': 'Crochet', 'support_email': settings.SUPPORT_EMAIL}
|
||||||
|
_send_account_notice(
|
||||||
|
'crochet/email/password_changed_subject.txt',
|
||||||
|
'crochet/email/password_changed_notice.txt',
|
||||||
|
'crochet/email/password_changed_notice.html',
|
||||||
|
context, user.email,
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
class StyledPasswordResetForm(PasswordResetForm):
|
class StyledPasswordResetForm(PasswordResetForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -79,15 +109,10 @@ class EmailUpdateForm(forms.Form):
|
|||||||
# todavía puede leerlo. Si no había email antes, o si "cambiar" no
|
# todavía puede leerlo. Si no había email antes, o si "cambiar" no
|
||||||
# ha cambiado nada, no hay a quién avisar ni de qué.
|
# ha cambiado nada, no hay a quién avisar ni de qué.
|
||||||
if old_email and old_email != self.user.email:
|
if old_email and old_email != self.user.email:
|
||||||
self._notify_previous_email(old_email)
|
|
||||||
|
|
||||||
def _notify_previous_email(self, old_email):
|
|
||||||
context = {'user': self.user, 'old_email': old_email, 'site_name': 'Crochet', 'support_email': settings.SUPPORT_EMAIL}
|
context = {'user': self.user, 'old_email': old_email, 'site_name': 'Crochet', 'support_email': settings.SUPPORT_EMAIL}
|
||||||
subject = ''.join(render_to_string('crochet/email/email_changed_subject.txt', context).splitlines())
|
_send_account_notice(
|
||||||
send_mail(
|
'crochet/email/email_changed_subject.txt',
|
||||||
subject=subject,
|
'crochet/email/email_changed_notice.txt',
|
||||||
message=render_to_string('crochet/email/email_changed_notice.txt', context),
|
'crochet/email/email_changed_notice.html',
|
||||||
from_email=None,
|
context, old_email,
|
||||||
recipient_list=[old_email],
|
|
||||||
html_message=render_to_string('crochet/email/email_changed_notice.html', context),
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: crochet\n"
|
"Project-Id-Version: crochet\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-21 11:41+0000\n"
|
"POT-Creation-Date: 2026-07-21 11:59+0000\n"
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
#: crochet/forms.py:33 crochet/forms.py:66
|
#: crochet/forms.py:48 crochet/forms.py:96
|
||||||
#: crochet/templates/crochet/account_settings.html:22
|
#: crochet/templates/crochet/account_settings.html:22
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr "Email"
|
msgstr "Email"
|
||||||
@@ -121,11 +121,15 @@ msgstr ""
|
|||||||
|
|
||||||
#: crochet/templates/crochet/email/email_changed_notice.html:12
|
#: crochet/templates/crochet/email/email_changed_notice.html:12
|
||||||
#: crochet/templates/crochet/email/email_changed_notice.txt:4
|
#: crochet/templates/crochet/email/email_changed_notice.txt:4
|
||||||
|
#: crochet/templates/crochet/email/password_changed_notice.html:12
|
||||||
|
#: crochet/templates/crochet/email/password_changed_notice.txt:4
|
||||||
msgid "Tu usuario:"
|
msgid "Tu usuario:"
|
||||||
msgstr "Your username:"
|
msgstr "Your username:"
|
||||||
|
|
||||||
#: crochet/templates/crochet/email/email_changed_notice.html:16
|
#: crochet/templates/crochet/email/email_changed_notice.html:16
|
||||||
#: crochet/templates/crochet/email/email_changed_notice.txt:6
|
#: crochet/templates/crochet/email/email_changed_notice.txt:6
|
||||||
|
#: crochet/templates/crochet/email/password_changed_notice.html:16
|
||||||
|
#: crochet/templates/crochet/email/password_changed_notice.txt:6
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Si no has sido tú quien ha hecho este cambio, contacta cuanto antes con "
|
"Si no has sido tú quien ha hecho este cambio, contacta cuanto antes con "
|
||||||
@@ -134,6 +138,18 @@ msgstr ""
|
|||||||
"If you didn't make this change, please contact technical support at "
|
"If you didn't make this change, please contact technical support at "
|
||||||
"%(support_email)s as soon as possible."
|
"%(support_email)s as soon as possible."
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/email/password_changed_notice.html:4
|
||||||
|
#: crochet/templates/crochet/email/password_changed_subject.txt:1
|
||||||
|
#, python-format
|
||||||
|
msgid "La contraseña de tu cuenta en %(site_name)s ha cambiado"
|
||||||
|
msgstr "The password on your %(site_name)s account has changed"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/email/password_changed_notice.html:8
|
||||||
|
#: crochet/templates/crochet/email/password_changed_notice.txt:2
|
||||||
|
#, python-format
|
||||||
|
msgid "La contraseña de tu cuenta en %(site_name)s ha cambiado."
|
||||||
|
msgstr "The password on your %(site_name)s account has changed."
|
||||||
|
|
||||||
#: crochet/templates/crochet/email/password_reset.html:4
|
#: crochet/templates/crochet/email/password_reset.html:4
|
||||||
#: crochet/templates/crochet/password_reset_done.html:4
|
#: crochet/templates/crochet/password_reset_done.html:4
|
||||||
#: crochet/templates/crochet/password_reset_form.html:4
|
#: crochet/templates/crochet/password_reset_form.html:4
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: crochet\n"
|
"Project-Id-Version: crochet\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-21 11:41+0000\n"
|
"POT-Creation-Date: 2026-07-21 11:59+0000\n"
|
||||||
"Language: es\n"
|
"Language: es\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
#: crochet/forms.py:33 crochet/forms.py:66
|
#: crochet/forms.py:48 crochet/forms.py:96
|
||||||
#: crochet/templates/crochet/account_settings.html:22
|
#: crochet/templates/crochet/account_settings.html:22
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -119,17 +119,33 @@ msgstr ""
|
|||||||
|
|
||||||
#: crochet/templates/crochet/email/email_changed_notice.html:12
|
#: crochet/templates/crochet/email/email_changed_notice.html:12
|
||||||
#: crochet/templates/crochet/email/email_changed_notice.txt:4
|
#: crochet/templates/crochet/email/email_changed_notice.txt:4
|
||||||
|
#: crochet/templates/crochet/email/password_changed_notice.html:12
|
||||||
|
#: crochet/templates/crochet/email/password_changed_notice.txt:4
|
||||||
msgid "Tu usuario:"
|
msgid "Tu usuario:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: crochet/templates/crochet/email/email_changed_notice.html:16
|
#: crochet/templates/crochet/email/email_changed_notice.html:16
|
||||||
#: crochet/templates/crochet/email/email_changed_notice.txt:6
|
#: crochet/templates/crochet/email/email_changed_notice.txt:6
|
||||||
|
#: crochet/templates/crochet/email/password_changed_notice.html:16
|
||||||
|
#: crochet/templates/crochet/email/password_changed_notice.txt:6
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Si no has sido tú quien ha hecho este cambio, contacta cuanto antes con "
|
"Si no has sido tú quien ha hecho este cambio, contacta cuanto antes con "
|
||||||
"soporte técnico en %(support_email)s."
|
"soporte técnico en %(support_email)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/email/password_changed_notice.html:4
|
||||||
|
#: crochet/templates/crochet/email/password_changed_subject.txt:1
|
||||||
|
#, python-format
|
||||||
|
msgid "La contraseña de tu cuenta en %(site_name)s ha cambiado"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/email/password_changed_notice.html:8
|
||||||
|
#: crochet/templates/crochet/email/password_changed_notice.txt:2
|
||||||
|
#, python-format
|
||||||
|
msgid "La contraseña de tu cuenta en %(site_name)s ha cambiado."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: crochet/templates/crochet/email/password_reset.html:4
|
#: crochet/templates/crochet/email/password_reset.html:4
|
||||||
#: crochet/templates/crochet/password_reset_done.html:4
|
#: crochet/templates/crochet/password_reset_done.html:4
|
||||||
#: crochet/templates/crochet/password_reset_form.html:4
|
#: crochet/templates/crochet/password_reset_form.html:4
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{% extends 'crochet/email/base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% blocktrans %}La contraseña de tu cuenta en {{ site_name }} ha cambiado{% endblocktrans %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p style="margin:0 0 16px 0;">
|
||||||
|
{% blocktrans %}La contraseña de tu cuenta en {{ site_name }} ha cambiado.{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin:0 0 16px 0; font-size:13px; color:#2e2118;">
|
||||||
|
{% trans 'Tu usuario:' %} {{ user.get_username }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin:0; font-weight:600;">
|
||||||
|
{% blocktrans %}Si no has sido tú quien ha hecho este cambio, contacta cuanto antes con soporte técnico en {{ support_email }}.{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{% load i18n %}{% autoescape off %}
|
||||||
|
{% blocktrans %}La contraseña de tu cuenta en {{ site_name }} ha cambiado.{% endblocktrans %}
|
||||||
|
|
||||||
|
{% trans 'Tu usuario:' %} {{ user.get_username }}
|
||||||
|
|
||||||
|
{% blocktrans %}Si no has sido tú quien ha hecho este cambio, contacta cuanto antes con soporte técnico en {{ support_email }}.{% endblocktrans %}
|
||||||
|
{% endautoescape %}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{% load i18n %}{% blocktrans %}La contraseña de tu cuenta en {{ site_name }} ha cambiado{% endblocktrans %}
|
||||||
@@ -116,7 +116,9 @@ class AccountEmailUpdateViewTests(TestCase):
|
|||||||
|
|
||||||
class AccountPasswordChangeViewTests(TestCase):
|
class AccountPasswordChangeViewTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = User.objects.create_user(username='crocheter', password='the-old-password-1')
|
self.user = User.objects.create_user(
|
||||||
|
username='crocheter', password='the-old-password-1', email='crocheter@example.com',
|
||||||
|
)
|
||||||
with translation.override('es'):
|
with translation.override('es'):
|
||||||
self.url = reverse('crochet:account_settings_password')
|
self.url = reverse('crochet:account_settings_password')
|
||||||
|
|
||||||
@@ -177,3 +179,43 @@ class AccountPasswordChangeViewTests(TestCase):
|
|||||||
self.assertContains(response, 'class="text-error text-xs"')
|
self.assertContains(response, 'class="text-error text-xs"')
|
||||||
self.user.refresh_from_db()
|
self.user.refresh_from_db()
|
||||||
self.assertTrue(self.user.check_password('the-old-password-1'))
|
self.assertTrue(self.user.check_password('the-old-password-1'))
|
||||||
|
|
||||||
|
def test_valid_password_change_notifies_the_users_email(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
self.client.post(self.url, {
|
||||||
|
'old_password': 'the-old-password-1',
|
||||||
|
'new_password1': 'the-new-password-2',
|
||||||
|
'new_password2': 'the-new-password-2',
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
message = mail.outbox[0]
|
||||||
|
self.assertEqual(message.to, ['crocheter@example.com'])
|
||||||
|
self.assertIn('crocheter', message.body)
|
||||||
|
self.assertIn('soporte@localhost', message.body)
|
||||||
|
html_bodies = [content for content, mimetype in message.alternatives if mimetype == 'text/html']
|
||||||
|
self.assertEqual(len(html_bodies), 1)
|
||||||
|
|
||||||
|
def test_user_without_an_email_gets_no_notification(self):
|
||||||
|
user_without_email = User.objects.create_user(username='noemail', password='the-old-password-1')
|
||||||
|
self.client.force_login(user_without_email)
|
||||||
|
|
||||||
|
self.client.post(self.url, {
|
||||||
|
'old_password': 'the-old-password-1',
|
||||||
|
'new_password1': 'the-new-password-2',
|
||||||
|
'new_password2': 'the-new-password-2',
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
|
||||||
|
def test_wrong_old_password_does_not_send_a_notification(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
self.client.post(self.url, {
|
||||||
|
'old_password': 'wrong-password',
|
||||||
|
'new_password1': 'the-new-password-2',
|
||||||
|
'new_password2': 'the-new-password-2',
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user