From cc8e2979317559d042a31be127e9dcddf66ecb7c Mon Sep 17 00:00:00 2001 From: Slavi Date: Mon, 25 Apr 2022 15:05:08 +0300 Subject: [PATCH 1/3] Password reset through default django implementation. --- .../registration/password_reset_complete.html | 8 ++++++++ .../registration/password_reset_confirm.html | 13 +++++++++++++ templates/registration/password_reset_done.html | 8 ++++++++ templates/registration/password_reset_email.html | 12 ++++++++++++ templates/registration/password_reset_form.html | 13 +++++++++++++ users/urls.py | 4 ++++ users/views.py | 15 ++++++++++++++- 7 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 templates/registration/password_reset_complete.html create mode 100644 templates/registration/password_reset_confirm.html create mode 100644 templates/registration/password_reset_done.html create mode 100644 templates/registration/password_reset_email.html create mode 100644 templates/registration/password_reset_form.html diff --git a/templates/registration/password_reset_complete.html b/templates/registration/password_reset_complete.html new file mode 100644 index 0000000..76188aa --- /dev/null +++ b/templates/registration/password_reset_complete.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block title %}Password reset.{% endblock %} + +{% block content %} +

Password reset.

+

Your password has been successfully reset.

+{% endblock %} \ No newline at end of file diff --git a/templates/registration/password_reset_confirm.html b/templates/registration/password_reset_confirm.html new file mode 100644 index 0000000..cdcbd1a --- /dev/null +++ b/templates/registration/password_reset_confirm.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block title %}Reset your password{% endblock %} + +{% block content %} +

Reset your password

+

Use the form below to reset your password.

+
+ {{ form.as_p }} +

+ {% csrf_token %} +
+{% endblock %} diff --git a/templates/registration/password_reset_done.html b/templates/registration/password_reset_done.html new file mode 100644 index 0000000..d4034f6 --- /dev/null +++ b/templates/registration/password_reset_done.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block title %}Password reset email sent.{% endblock %} + +{% block content %} +

Password reset email sent.

+

Please check your email to change your password.

+{% endblock %} \ No newline at end of file diff --git a/templates/registration/password_reset_email.html b/templates/registration/password_reset_email.html new file mode 100644 index 0000000..9374031 --- /dev/null +++ b/templates/registration/password_reset_email.html @@ -0,0 +1,12 @@ +{% autoescape off %} + + +Hi {{user.username}} + +Please use the link below to verify your account. + + +http://{{domain}}{% url 'users:password_reset_confirm' uidb64=uid token=token %} + + +{% endautoescape %} \ No newline at end of file diff --git a/templates/registration/password_reset_form.html b/templates/registration/password_reset_form.html new file mode 100644 index 0000000..b6f4e5c --- /dev/null +++ b/templates/registration/password_reset_form.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block title %}Reset password through email.{% endblock %} + +{% block content %} +

Reset your password through email.

+

Fill in your email to reset your password.

+
+ {{ form.as_p }} +

+ {% csrf_token %} +
+{% endblock %} \ No newline at end of file diff --git a/users/urls.py b/users/urls.py index bf8ba18..ad09bb7 100644 --- a/users/urls.py +++ b/users/urls.py @@ -2,10 +2,14 @@ from django.contrib.auth.decorators import login_required from . import views from django.views.generic import TemplateView +from django.contrib.auth.views import PasswordResetDoneView, PasswordResetCompleteView urlpatterns = [ path('register/', views.SignUpView.as_view(), name='register'), path('login/', views.CustomLoginView.as_view(), name='login'), + path("password_change/", views.CustomPasswordChangeView.as_view(), name="password_change"), + path("password_reset/", views.CustomPasswordResetView.as_view(), name="password_reset"), + path("reset///", views.CustomPasswordResetConfirmView.as_view(), name="password_reset_confirm"), path('', include('django.contrib.auth.urls')), path('dashboard/', login_required(TemplateView.as_view(template_name="users/dashboard.html")), name='dashboard'), path('edit/', views.EditView.as_view(), name='edit_account'), diff --git a/users/views.py b/users/views.py index 1f09681..7120b8d 100644 --- a/users/views.py +++ b/users/views.py @@ -17,6 +17,8 @@ from django.utils.encoding import force_str from .utilities import generate_token, send_verification_email +from django.contrib.auth.views import PasswordResetView, PasswordResetConfirmView, PasswordChangeView + class SignUpView(SuccessMessageMixin, CreateView): template_name = 'registration/register.html' success_url = reverse_lazy('users:login') @@ -125,4 +127,15 @@ def get(self, request, uidb64, token): messages.add_message(request, messages.ERROR, 'Something went wrong with your link.') - return render(request, 'registration/activate-fail.html', {"user": user}) \ No newline at end of file + return render(request, 'registration/activate-fail.html', {"user": user}) + +# Defining custom classes to successfully reverse django.contrib.auth.urls, +# since they're in the users' app +class CustomPasswordResetView(PasswordResetView): + success_url = reverse_lazy("users:password_reset_done") + +class CustomPasswordResetConfirmView(PasswordResetConfirmView): + success_url = reverse_lazy("users:password_reset_complete") + +class CustomPasswordChangeView(PasswordChangeView): + success_url = reverse_lazy("users:password_change_done") \ No newline at end of file From 8886280eba8c65b87d2fd627c22ea1e9d12d009f Mon Sep 17 00:00:00 2001 From: Slavi Date: Thu, 28 Apr 2022 13:39:36 +0300 Subject: [PATCH 2/3] Formatted code and cleaned up password reset. --- .../registration/password_change_done.html | 8 -- .../registration/password_reset_complete.html | 8 -- .../registration/password_reset_done.html | 3 +- templates/registration/reset-fail.html | 6 ++ users/views.py | 78 +++++++++++++++++-- 5 files changed, 79 insertions(+), 24 deletions(-) delete mode 100644 templates/registration/password_change_done.html delete mode 100644 templates/registration/password_reset_complete.html create mode 100644 templates/registration/reset-fail.html diff --git a/templates/registration/password_change_done.html b/templates/registration/password_change_done.html deleted file mode 100644 index b7fd07c..0000000 --- a/templates/registration/password_change_done.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Password changed{% endblock %} - -{% block content %} -

Password changed

-

Your password has been successfully changed.

-{% endblock %} diff --git a/templates/registration/password_reset_complete.html b/templates/registration/password_reset_complete.html deleted file mode 100644 index 76188aa..0000000 --- a/templates/registration/password_reset_complete.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Password reset.{% endblock %} - -{% block content %} -

Password reset.

-

Your password has been successfully reset.

-{% endblock %} \ No newline at end of file diff --git a/templates/registration/password_reset_done.html b/templates/registration/password_reset_done.html index d4034f6..956a62a 100644 --- a/templates/registration/password_reset_done.html +++ b/templates/registration/password_reset_done.html @@ -3,6 +3,5 @@ {% block title %}Password reset email sent.{% endblock %} {% block content %} -

Password reset email sent.

-

Please check your email to change your password.

+

Haven't received an email? Resend email for password reset here.

{% endblock %} \ No newline at end of file diff --git a/templates/registration/reset-fail.html b/templates/registration/reset-fail.html new file mode 100644 index 0000000..bf176b7 --- /dev/null +++ b/templates/registration/reset-fail.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} +{% block title %}Password reset failed{% endblock %} + +{% block content %} +

Resend email for password reset here.

+{% endblock %} \ No newline at end of file diff --git a/users/views.py b/users/views.py index 7120b8d..ce2e900 100644 --- a/users/views.py +++ b/users/views.py @@ -13,12 +13,23 @@ from django.contrib.auth import login as auth_login from django.contrib.auth.views import logout_then_login +from django.contrib.auth import logout as auth_logout from django.utils.http import urlsafe_base64_decode from django.utils.encoding import force_str from .utilities import generate_token, send_verification_email from django.contrib.auth.views import PasswordResetView, PasswordResetConfirmView, PasswordChangeView +INTERNAL_RESET_SESSION_TOKEN = "_password_reset_token" +from django.core.exceptions import ImproperlyConfigured +from django.http import HttpResponseRedirect +from django.utils.decorators import method_decorator +from django.views.decorators.cache import never_cache +from django.views.decorators.debug import sensitive_post_parameters + +from django.utils.safestring import mark_safe +from django.contrib.auth import update_session_auth_hash + class SignUpView(SuccessMessageMixin, CreateView): template_name = 'registration/register.html' success_url = reverse_lazy('users:login') @@ -130,12 +141,67 @@ def get(self, request, uidb64, token): return render(request, 'registration/activate-fail.html', {"user": user}) # Defining custom classes to successfully reverse django.contrib.auth.urls, -# since they're in the users' app -class CustomPasswordResetView(PasswordResetView): +# since they're in the users' app and use django messages framework +class CustomPasswordResetView(SuccessMessageMixin, PasswordResetView): success_url = reverse_lazy("users:password_reset_done") + success_message = "Password reset email sent. Check your email to reset your password." + + def dispatch(self, request, *args, **kwargs): + if request.user.is_authenticated: + return redirect('users:password_change') -class CustomPasswordResetConfirmView(PasswordResetConfirmView): - success_url = reverse_lazy("users:password_reset_complete") + return super().dispatch(request, *args, **kwargs) -class CustomPasswordChangeView(PasswordChangeView): - success_url = reverse_lazy("users:password_change_done") \ No newline at end of file +class CustomPasswordResetConfirmView(SuccessMessageMixin ,PasswordResetConfirmView): + success_url = reverse_lazy("users:login") + success_message = mark_safe("Your password has been successfully reset.
You can now login with your new password.") + + #Copied from PasswordResetConfirmView, changed the unsuccessful page redirect + @method_decorator(sensitive_post_parameters()) + @method_decorator(never_cache) + def dispatch(self, *args, **kwargs): + if "uidb64" not in kwargs or "token" not in kwargs: + raise ImproperlyConfigured( + "The URL path must contain 'uidb64' and 'token' parameters." + ) + + self.validlink = False + self.user = self.get_user(kwargs["uidb64"]) + + if self.user is not None: + token = kwargs["token"] + if token == self.reset_url_token: + session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN) + if self.token_generator.check_token(self.user, session_token): + # If the token is valid, display the password reset form. + self.validlink = True + return super().dispatch(*args, **kwargs) + else: + if self.token_generator.check_token(self.user, token): + # Store the token in the session and redirect to the + # password reset form at a URL without the token. That + # avoids the possibility of leaking the token in the + # HTTP Referer header. + self.request.session[INTERNAL_RESET_SESSION_TOKEN] = token + redirect_url = self.request.path.replace( + token, self.reset_url_token + ) + return HttpResponseRedirect(redirect_url) + + # Display the "Password reset unsuccessful" page. + messages.add_message(self.request, messages.ERROR, + 'Something went wrong with your link.') + return render(self.request, 'registration/reset-fail.html', {"user": self.user}) + +class CustomPasswordChangeView(SuccessMessageMixin ,PasswordChangeView): + success_url = reverse_lazy("users:login") + success_message = mark_safe("Your password has been successfully changed.
You can log in with your new password.") + + def form_valid(self, form): + form.save() + # Updating the password logs out all other sessions for the user + # except the current one. + update_session_auth_hash(self.request, form.user) + #Make the user use his new password + auth_logout(self.request) + return super().form_valid(form) \ No newline at end of file From cccafdfbd705d16706f66c80c69ddc04af788725 Mon Sep 17 00:00:00 2001 From: Slavi Date: Thu, 28 Apr 2022 17:08:26 +0300 Subject: [PATCH 3/3] Fixed error messages on forms. --- templates/registration/login.html | 1 + .../registration/password_change_form.html | 4 +- .../registration/password_reset_confirm.html | 4 +- .../registration/password_reset_form.html | 5 +- .../resend-email-verification.html | 2 + users/forms.py | 96 +++++++++++++++++-- users/views.py | 44 ++++----- 7 files changed, 123 insertions(+), 33 deletions(-) diff --git a/templates/registration/login.html b/templates/registration/login.html index 9421d6e..c5b4ea7 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -8,6 +8,7 @@ {% csrf_token %} {{form|crispy}}

Don't have an account? Create one here.

+

Forgot your password? Reset it here.

Resend email verification here.

diff --git a/templates/registration/password_change_form.html b/templates/registration/password_change_form.html index f3a50a0..e96329b 100644 --- a/templates/registration/password_change_form.html +++ b/templates/registration/password_change_form.html @@ -1,4 +1,6 @@ {% extends "base.html" %} +{% load crispy_forms_filters %} +{% load crispy_forms_tags %} {% block title %}Change your password{% endblock %} @@ -6,7 +8,7 @@

Change your password

Use the form below to change your password.

- {{ form.as_p }} + {{ form|crispy }}

{% csrf_token %}
diff --git a/templates/registration/password_reset_confirm.html b/templates/registration/password_reset_confirm.html index cdcbd1a..1d7c853 100644 --- a/templates/registration/password_reset_confirm.html +++ b/templates/registration/password_reset_confirm.html @@ -1,4 +1,6 @@ {% extends "base.html" %} +{% load crispy_forms_filters %} +{% load crispy_forms_tags %} {% block title %}Reset your password{% endblock %} @@ -6,7 +8,7 @@

Reset your password

Use the form below to reset your password.

- {{ form.as_p }} + {{ form|crispy }}

{% csrf_token %}
diff --git a/templates/registration/password_reset_form.html b/templates/registration/password_reset_form.html index b6f4e5c..22cbf6d 100644 --- a/templates/registration/password_reset_form.html +++ b/templates/registration/password_reset_form.html @@ -1,4 +1,6 @@ {% extends "base.html" %} +{% load crispy_forms_filters %} +{% load crispy_forms_tags %} {% block title %}Reset password through email.{% endblock %} @@ -6,7 +8,8 @@

Reset your password through email.

Fill in your email to reset your password.

- {{ form.as_p }} + {{ form|crispy}} +

Resend email verification here.

{% csrf_token %}
diff --git a/templates/registration/resend-email-verification.html b/templates/registration/resend-email-verification.html index f4eb997..35b2e01 100644 --- a/templates/registration/resend-email-verification.html +++ b/templates/registration/resend-email-verification.html @@ -4,6 +4,8 @@ {% load crispy_forms_tags %} {% block content %} +

Resend email verification.

+

Fill in your email to receive another verification.

{% csrf_token %} {{form|crispy}} diff --git a/users/forms.py b/users/forms.py index c0ce992..4f407aa 100644 --- a/users/forms.py +++ b/users/forms.py @@ -1,10 +1,16 @@ +from urllib import request from django import forms -from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, UsernameField, UserChangeForm +from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, UsernameField, PasswordResetForm from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ +from django.utils.safestring import mark_safe from .models import CustomUser +from django.core.exceptions import ValidationError +from .utilities import send_verification_email +from django.contrib.auth import authenticate + class CustomUserCreationForm(UserCreationForm): class Meta: @@ -30,11 +36,89 @@ class CustomUserAuthenticationForm(AuthenticationForm): error_messages = { **AuthenticationForm.error_messages, 'invalid_login': _( - "Please enter a correct username or email, and password. Note that both fields may be case-sensitive." + mark_safe("Please enter a correct username or email, and password.
Note that both fields may be case-sensitive.") ), } -class ResendEmailVerificationForm(ModelForm): - class Meta: - model = CustomUser - fields = ("email",) \ No newline at end of file + def clean(self): + username = self.cleaned_data.get("username") + password = self.cleaned_data.get("password") + if username is not None and password: + self.user_cache = authenticate( + self.request, username=username, password=password + ) + if self.user_cache is None: + self.add_error('username', ValidationError( + self.error_messages["invalid_login"], + code="invalid_login", + params={"username": self.username_field.verbose_name}, + )) + self.add_error('password', '') + else: + self.confirm_login_allowed(self.user_cache) + return self.cleaned_data + + +class ResendEmailVerificationForm(forms.Form): + email = forms.EmailField( + label=_("Email"), + max_length=254, + widget=forms.EmailInput(attrs={"autocomplete": "email"}), + ) + + error_messages = { + "user_not_found": _("User with this email not found!"), + "email_already_verified": _("User\'s email already verified!"), + } + + def clean_email(self): + email = self.cleaned_data['email'] + try: + self.user = CustomUser.objects.get(email__iexact=email) + except Exception as e: + self.user = None + + if self.user: + if self.user.email_verified == True: + raise ValidationError( + self.error_messages["email_already_verified"], + code="email_already_verified", + ) + else: + raise ValidationError( + self.error_messages["user_not_found"], + code="user_not_found", + ) + + return email + + def save(self, request): + send_verification_email(request, self.user) + + +class CustomPasswordResetForm(PasswordResetForm): + error_messages = { + "user_not_found": _("User with this email not found!"), + "email_not_verified": _("User's email is not verified!"), + } + + def clean_email(self): + email = self.cleaned_data['email'] + try: + self.user = CustomUser.objects.get(email__iexact=email) + except Exception as e: + self.user = None + + if self.user: + if self.user.email_verified == False: + raise ValidationError( + self.error_messages["email_not_verified"], + code="email_not_verified", + ) + else: + raise ValidationError( + self.error_messages["user_not_found"], + code="user_not_found", + ) + + return email \ No newline at end of file diff --git a/users/views.py b/users/views.py index ce2e900..0db3144 100644 --- a/users/views.py +++ b/users/views.py @@ -7,7 +7,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import get_object_or_404, redirect, render -from users.forms import CustomUserCreationForm, CustomUserChangeForm, CustomUserAuthenticationForm, ResendEmailVerificationForm +from users import forms as users_forms from django.contrib.auth.views import LoginView from users.models import CustomUser @@ -30,10 +30,11 @@ from django.utils.safestring import mark_safe from django.contrib.auth import update_session_auth_hash + class SignUpView(SuccessMessageMixin, CreateView): template_name = 'registration/register.html' success_url = reverse_lazy('users:login') - form_class = CustomUserCreationForm + form_class = users_forms.CustomUserCreationForm def form_valid(self, form): """If the form is valid, save the associated model.""" @@ -50,10 +51,11 @@ def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) + class EditView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): template_name = 'users/edit_account.html' success_url = reverse_lazy('users:edit_account') - form_class = CustomUserChangeForm + form_class = users_forms.CustomUserChangeForm success_message = "Successfully updated profile." def form_valid(self, form): @@ -71,8 +73,9 @@ def form_valid(self, form): def get_object(self): return get_object_or_404(CustomUser, pk=self.request.user.id) + class CustomLoginView(LoginView): - authentication_form = CustomUserAuthenticationForm + authentication_form = users_forms.CustomUserAuthenticationForm redirect_authenticated_user = True #success_url not needed. There's a LOGIN_REDIRECT_URL in base.py settings. @@ -87,11 +90,12 @@ class CustomLoginView(LoginView): # messages.success(self.request, 'Successfully signed in.') # return redirect(self.get_success_url()) + class ResendEmailVerificationView(SuccessMessageMixin, FormView): template_name = 'registration/resend-email-verification.html' success_url = reverse_lazy('users:login') - form_class = ResendEmailVerificationForm - #Can't use success_message, cos it attaches to form_valid + form_class = users_forms.ResendEmailVerificationForm + success_message = "Email verification resent. Please verify your email to log in." def dispatch(self, request, *args, **kwargs): if request.user.is_authenticated: @@ -99,25 +103,13 @@ def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) - def post(self, request, *args, **kwargs): - form = self.get_form() - - try: - self.user = CustomUser.objects.get(email__iexact=form['email'].value()) #form.cleaned_data is accessible after running form.is_valid(), which works for non-registered users only. - except Exception as e: - self.user = None - - if self.user: - if self.user.email_verified == False: - send_verification_email(self.request, self.user) - messages.success(self.request, 'Email verification resent. Please verify your email to log in.') - return redirect(self.get_success_url()) - else: - messages.error(self.request, 'User\'s email already verified!') - else: - messages.error(self.request, 'User with this email not found!') + def form_valid(self, form): + opts = { + "request": self.request, + } + form.save(**opts) + return super().form_valid(form) - return render(self.request, self.template_name, { 'form': self.form_class }) class ActivateUserView(View): @@ -140,9 +132,11 @@ def get(self, request, uidb64, token): 'Something went wrong with your link.') return render(request, 'registration/activate-fail.html', {"user": user}) + # Defining custom classes to successfully reverse django.contrib.auth.urls, # since they're in the users' app and use django messages framework class CustomPasswordResetView(SuccessMessageMixin, PasswordResetView): + form_class = users_forms.CustomPasswordResetForm success_url = reverse_lazy("users:password_reset_done") success_message = "Password reset email sent. Check your email to reset your password." @@ -152,6 +146,7 @@ def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) + class CustomPasswordResetConfirmView(SuccessMessageMixin ,PasswordResetConfirmView): success_url = reverse_lazy("users:login") success_message = mark_safe("Your password has been successfully reset.
You can now login with your new password.") @@ -193,6 +188,7 @@ def dispatch(self, *args, **kwargs): 'Something went wrong with your link.') return render(self.request, 'registration/reset-fail.html', {"user": self.user}) + class CustomPasswordChangeView(SuccessMessageMixin ,PasswordChangeView): success_url = reverse_lazy("users:login") success_message = mark_safe("Your password has been successfully changed.
You can log in with your new password.")