Add password confirmation, validation, and email notification for password changes

This commit is contained in:
pacnpal
2024-11-13 15:17:07 +00:00
parent 5a1fdb6d16
commit 1ee4b00961
3 changed files with 134 additions and 5 deletions

View File

@@ -30,6 +30,7 @@ from django_htmx.http import HttpResponseClientRefresh
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.contrib.sites.requests import RequestSite from django.contrib.sites.requests import RequestSite
from contextlib import suppress from contextlib import suppress
import re
if TYPE_CHECKING: if TYPE_CHECKING:
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
@@ -193,18 +194,61 @@ class SettingsView(LoginRequiredMixin, TemplateView):
user.save() user.save()
messages.success(request, 'Profile updated successfully') 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]: def _handle_password_change(self, request: HttpRequest) -> Optional[HttpResponseRedirect]:
user = cast(User, request.user) user = cast(User, request.user)
old_password = request.POST.get('old_password', '') old_password = request.POST.get('old_password', '')
new_password = request.POST.get('new_password', '') new_password = request.POST.get('new_password', '')
confirm_password = request.POST.get('confirm_password', '')
if not user.check_password(old_password): if not user.check_password(old_password):
messages.error(request, 'Current password is incorrect') messages.error(request, 'Current password is incorrect')
return None 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.set_password(new_password)
user.save() 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')) return HttpResponseRedirect(reverse('account_login'))
def _handle_email_change(self, request: HttpRequest) -> None: def _handle_email_change(self, request: HttpRequest) -> None:

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.container {
background-color: #f9f9f9;
border-radius: 5px;
padding: 20px;
margin-top: 20px;
}
.header {
color: #2563eb;
font-size: 24px;
margin-bottom: 20px;
}
.footer {
margin-top: 30px;
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
Password Changed Successfully
</div>
<p>Hi {{ user.username }},</p>
<p>This email confirms that your password was recently changed on {{ site_name }}.</p>
<p>If you did not make this change, please contact our support team immediately.</p>
<div class="footer">
<p>Best regards,<br>The {{ site_name }} Team</p>
</div>
</div>
</body>
</html>

View File

@@ -45,7 +45,17 @@
</div> </div>
<div class="p-6 mt-6 overflow-hidden bg-white rounded-lg shadow dark:bg-gray-800"> <div class="p-6 mt-6 overflow-hidden bg-white rounded-lg shadow dark:bg-gray-800">
<form method="POST"> <form method="POST" x-data="{
newPassword: '',
confirmPassword: '',
passwordsMatch() { return this.newPassword === this.confirmPassword },
isValidPassword() {
return this.newPassword.length >= 8 &&
/[A-Z]/.test(this.newPassword) &&
/[a-z]/.test(this.newPassword) &&
/[0-9]/.test(this.newPassword);
}
}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="action" value="change_password"> <input type="hidden" name="action" value="change_password">
@@ -53,15 +63,46 @@
<div class="mb-4"> <div class="mb-4">
<label for="old_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Current Password</label> <label for="old_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Current Password</label>
<input type="password" name="old_password" id="old_password" class="block w-full mt-1 border-gray-300 rounded-md shadow-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300"> <input type="password" name="old_password" id="old_password" required class="block w-full mt-1 border-gray-300 rounded-md shadow-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="new_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300">New Password</label> <label for="new_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300">New Password</label>
<input type="password" name="new_password" id="new_password" class="block w-full mt-1 border-gray-300 rounded-md shadow-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300"> <input
type="password"
name="new_password"
id="new_password"
x-model="newPassword"
required
class="block w-full mt-1 border-gray-300 rounded-md shadow-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300"
>
<div class="mt-1 text-sm text-gray-500 dark:text-gray-400" x-show="newPassword && !isValidPassword()">
Password must be at least 8 characters and contain uppercase, lowercase, and numbers
</div>
</div> </div>
<button type="submit" class="px-4 py-2 text-white bg-blue-500 rounded-md">Change Password</button> <div class="mb-4">
<label for="confirm_password" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Confirm New Password</label>
<input
type="password"
name="confirm_password"
id="confirm_password"
x-model="confirmPassword"
required
class="block w-full mt-1 border-gray-300 rounded-md shadow-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300"
>
<div class="mt-1 text-sm text-red-500" x-show="confirmPassword && !passwordsMatch()">
Passwords do not match
</div>
</div>
<button
type="submit"
class="px-4 py-2 text-white bg-blue-500 rounded-md disabled:opacity-50"
x-bind:disabled="!passwordsMatch() || !isValidPassword()"
>
Change Password
</button>
</form> </form>
</div> </div>
</div> </div>