From 1ee4b0096133e1fb27129f9e069711f5743927a3 Mon Sep 17 00:00:00 2001 From: pacnpal <183241239+pacnpal@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:17:07 +0000 Subject: [PATCH] Add password confirmation, validation, and email notification for password changes --- accounts/views.py | 46 ++++++++++++++++- .../email/password_change_confirmation.html | 44 +++++++++++++++++ templates/accounts/settings.html | 49 +++++++++++++++++-- 3 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 templates/accounts/email/password_change_confirmation.html diff --git a/accounts/views.py b/accounts/views.py index 92bba1e1..dbc2145c 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -30,6 +30,7 @@ from django_htmx.http import HttpResponseClientRefresh from django.contrib.sites.models import Site from django.contrib.sites.requests import RequestSite from contextlib import suppress +import re if TYPE_CHECKING: from django.contrib.sites.models import Site @@ -193,18 +194,61 @@ class SettingsView(LoginRequiredMixin, TemplateView): user.save() messages.success(request, 'Profile updated successfully') + def _validate_password(self, password: str) -> bool: + """Validate password meets requirements.""" + if len(password) < 8: + return False + if not re.search(r'[A-Z]', password): + return False + if not re.search(r'[a-z]', password): + return False + if not re.search(r'[0-9]', password): + return False + return True + + def _send_password_change_confirmation(self, request: HttpRequest, user: User) -> None: + """Send password change confirmation email.""" + site = get_current_site(request) + context = { + 'user': user, + 'site_name': site.name, + } + + email_html = render_to_string('accounts/email/password_change_confirmation.html', context) + + EmailService.send_email( + to=user.email, + subject='Password Changed Successfully', + text='Your password has been changed successfully.', + site=site, + html=email_html + ) + def _handle_password_change(self, request: HttpRequest) -> Optional[HttpResponseRedirect]: user = cast(User, request.user) old_password = request.POST.get('old_password', '') new_password = request.POST.get('new_password', '') + confirm_password = request.POST.get('confirm_password', '') if not user.check_password(old_password): messages.error(request, 'Current password is incorrect') return None + if new_password != confirm_password: + messages.error(request, 'New passwords do not match') + return None + + if not self._validate_password(new_password): + messages.error(request, 'Password must be at least 8 characters and contain uppercase, lowercase, and numbers') + return None + user.set_password(new_password) user.save() - messages.success(request, 'Password changed successfully') + + # Send confirmation email + self._send_password_change_confirmation(request, user) + + messages.success(request, 'Password changed successfully. Please check your email for confirmation.') return HttpResponseRedirect(reverse('account_login')) def _handle_email_change(self, request: HttpRequest) -> None: diff --git a/templates/accounts/email/password_change_confirmation.html b/templates/accounts/email/password_change_confirmation.html new file mode 100644 index 00000000..dc69c480 --- /dev/null +++ b/templates/accounts/email/password_change_confirmation.html @@ -0,0 +1,44 @@ + + +
+ + + +Hi {{ user.username }},
+This email confirms that your password was recently changed on {{ site_name }}.
+If you did not make this change, please contact our support team immediately.
+ +