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 @@ + + + + + + +
+
+ Password Changed Successfully +
+

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.

+ +
+ + diff --git a/templates/accounts/settings.html b/templates/accounts/settings.html index ccf0ee56..1031b726 100644 --- a/templates/accounts/settings.html +++ b/templates/accounts/settings.html @@ -45,7 +45,17 @@
-
+ {% csrf_token %} @@ -53,15 +63,46 @@
- +
- + +
+ Password must be at least 8 characters and contain uppercase, lowercase, and numbers +
- +
+ + +
+ Passwords do not match +
+
+ +