Password reset

This commit is contained in:
Klaas van Schelven
2024-05-30 12:35:14 +02:00
parent 3054834585
commit 142c704682
12 changed files with 753 additions and 8 deletions

View File

@@ -7,7 +7,7 @@ from django.contrib.auth import views as auth_views
from alerts.views import debug_email as debug_alerts_email
from users.views import debug_email as debug_users_email
from bugsink.app_settings import get_settings
from users.views import signup, confirm_email, resend_confirmation
from users.views import signup, confirm_email, resend_confirmation, request_reset_password, reset_password
from .views import home, trigger_error, favicon
@@ -24,6 +24,9 @@ urlpatterns = [
path("accounts/resend-confirmation/", resend_confirmation, name="resend_confirmation"),
path("accounts/confirm-email/<str:token>/", confirm_email, name="confirm_email"),
path("accounts/request-reset-password/", request_reset_password, name="request_reset_password"),
path("accounts/reset-password/<str:token>/", reset_password, name="reset_password"),
path("accounts/login/", auth_views.LoginView.as_view(template_name="bugsink/login.html"), name="login"),
path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout"),

View File

@@ -44,7 +44,7 @@
</form>
<div class="mt-4">
<a href="{#{% url 'password_reset' TODO %}#}" class="text-slate-800">Forgot password?</a>
<a href="{% url 'request_reset_password' %}" class="text-slate-800">Forgot password?</a>
{% if registration_enabled %}<a href="{% url 'signup' %}" class="float-right text-slate-800">Create an account</a>{% endif %}
</div>
</div>

View File

@@ -54,7 +54,7 @@
</form>
<div class="mt-4">
<a href="{#{% url 'password_reset' TODO %}#}" class="text-slate-800">Forgot password?</a>
<a href="{% url 'request_reset_password' %}" class="text-slate-800">Forgot password?</a>
<a href="{% url 'login' %}" class="float-right text-slate-800">Log in instead</a>
</div>
</div>

View File

@@ -2,7 +2,7 @@ import urllib.parse
from django import forms
from django.urls import reverse
from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm
from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm, SetPasswordForm as BaseSetPasswordForm
from django.core.validators import EmailValidator
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
@@ -11,6 +11,11 @@ from django.forms import ModelForm
from django.utils.html import escape, mark_safe
def _(x):
# dummy gettext
return x
UserModel = get_user_model()
@@ -68,3 +73,26 @@ class UserCreationForm(BaseUserCreationForm):
class ResendConfirmationForm(forms.Form):
email = forms.EmailField()
class RequestPasswordResetForm(forms.Form):
email = forms.EmailField()
def clean_email(self):
email = self.cleaned_data['email']
if not UserModel.objects.filter(username=email).exists():
# Many sites say "if the email is registered, we've sent you an email with a password reset link" instead.
# The idea is not to leak information about which emails are registered. But in our setup we're already
# leaking that information in the signup form. At least for now, I'm erring on the side of
# user-friendliness. see https://news.ycombinator.com/item?id=33718202
raise ValidationError("This email is not registered.")
return email
class SetPasswordForm(BaseSetPasswordForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['new_password1'].help_text = "At least 8 characters"
self.fields['new_password2'].help_text = None # "Confirm password" is descriptive enough

View File

@@ -19,3 +19,17 @@ def send_confirm_email(email, token):
"confirm_url": reverse("confirm_email", kwargs={"token": token}),
},
)
@shared_task
def send_reset_email(email, token):
send_rendered_email(
subject="Reset your password",
base_template_name="users/reset_password_email",
recipient_list=[email],
context={
"site_title": get_settings().SITE_TITLE,
"base_url": get_settings().BASE_URL + "/",
"reset_url": reverse("reset_email", kwargs={"token": token}),
},
)

View File

@@ -0,0 +1,41 @@
{% extends "barest_base.html" %}
{% load static %}
{% block title %}Resend confirmation · {{ site_title }}{% endblock %}
{% block content %}
<div class="bg-cyan-100 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #}
<div class="bg-white lg:w-5/12 md:6/12 w-10/12"> {# the centered box #}
<div class="bg-slate-200 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #}
<a href="/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16" alt="Bugsink"></a>
</div>
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">
<form method="post" action=".">
{% csrf_token %}
<div class="text-lg mb-6 md:mb-8">
<input name="email" type="text" class="{% if form.email.errors %}bg-red-100{% else %}bg-slate-200{% endif %} pl-4 py-2 md:py-4 focus:outline-none w-full" {% if form.email.value %}value="{{ form.email.value }}"{% endif %} placeholder="{{ form.email.label }}" />
{% if form.email.errors %}
{% for error in form.email.errors %}
<div class="text-red-500 pt-1 px-2 text-sm">{{ error }}</div>
{% endfor %}
{% elif form.email.help_text %}
<div class="text-gray-500 pt-1 px-2 text-sm">{{ form.email.help_text|safe }}</div>
{% endif %}
</div>
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">Reset password</button>
</form>
<div class="mt-4">
<a href="{% url 'login' %}" class="text-slate-800">Log in instead</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -31,7 +31,7 @@
</form>
<div class="mt-4">
<a href="{#{% url 'password_reset' TODO %}#}" class="text-slate-800">Forgot password?</a>
<a href="{% url 'request_reset_password' %}" class="text-slate-800">Forgot password?</a>
<a href="{% url 'login' %}" class="float-right text-slate-800">Log in instead</a>
</div>
</div>

View File

@@ -0,0 +1,53 @@
{% extends "barest_base.html" %}
{% load static %}
{% block title %}Reset password · {{ site_title }}{% endblock %}
{% block content %}
<div class="bg-cyan-100 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #}
<div class="bg-white lg:w-5/12 md:6/12 w-10/12"> {# the centered box #}
<div class="bg-slate-200 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #}
<a href="/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16" alt="Bugsink"></a>
</div>
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">
<form method="post" action=".">
{% csrf_token %}
<div class="text-lg mb-6 md:mb-8">
<input name="new_password1" type="password" class="{% if form.new_password1.errors %}bg-red-100{% else %}bg-slate-200{% endif %} pl-4 py-2 md:py-4 focus:outline-none w-full" {% if form.new_password1.value %}value="{{ form.new_password1.value }}"{% endif %} placeholder="{{ form.new_password1.label }}" />
{% if form.new_password1.errors %}
{% for error in form.new_password1.errors %}
<div class="text-red-500 pt-1 px-2 text-sm">{{ error }}</div>
{% endfor %}
{% elif form.new_password1.help_text %}
<div class="text-gray-500 pt-1 px-2 text-sm">{{ form.new_password1.help_text|safe }}</div>
{% endif %}
</div>
<div class="text-lg mb-6 md:mb-8">
<input name="new_password2" type="password" class="{% if form.new_password2.errors %}bg-red-100{% else %}bg-slate-200{% endif %} pl-4 py-2 md:py-4 focus:outline-none w-full" {% if form.new_password2.value %}value="{{ form.new_password2.value }}"{% endif %} placeholder="{{ form.new_password2.label }}" />
{% if form.new_password2.errors %}
{% for error in form.new_password2.errors %}
<div class="text-red-500 pt-1 px-2 text-sm">{{ error }}</div>
{% endfor %}
{% elif form.new_password2.help_text %}
<div class="text-gray-500 pt-1 px-2 text-sm">{{ form.new_password2.help_text|safe }}</div>
{% endif %}
</div>
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">Sign up</button>
</form>
<div class="mt-4">
<a href="{% url 'login' %}" class="text-slate-800">Log in instead</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,519 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="color-scheme: light dark; supported-color-schemes: light dark;">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<title></title>
<style type="text/css" rel="stylesheet" media="all">
/* Base ------------------------------ */
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&amp;display=swap");
body {
width: 100% !important;
height: 100%;
margin: 0;
-webkit-text-size-adjust: none;
}
a {
color: #3869D4;
}
a img {
border: none;
}
td {
word-break: break-word;
}
.preheader {
display: none !important;
visibility: hidden;
mso-hide: all;
font-size: 1px;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
}
/* Type ------------------------------ */
body,
td,
th {
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
}
h1 {
margin-top: 0;
color: #333333;
font-size: 22px;
font-weight: bold;
text-align: left;
}
h2 {
margin-top: 0;
color: #333333;
font-size: 16px;
font-weight: bold;
text-align: left;
}
h3 {
margin-top: 0;
color: #333333;
font-size: 14px;
font-weight: bold;
text-align: left;
}
td,
th {
font-size: 16px;
}
p,
ul,
ol,
blockquote {
margin: .4em 0 1.1875em;
font-size: 16px;
line-height: 1.625;
}
p.sub {
font-size: 13px;
}
/* Utilities ------------------------------ */
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.u-margin-bottom-none {
margin-bottom: 0;
}
/* Buttons ------------------------------ */
.button {
background-color: #3869D4;
border-top: 10px solid #3869D4;
border-right: 18px solid #3869D4;
border-bottom: 10px solid #3869D4;
border-left: 18px solid #3869D4;
display: inline-block;
color: #FFF;
text-decoration: none;
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
-webkit-text-size-adjust: none;
box-sizing: border-box;
}
.button--green {
background-color: #22BC66;
border-top: 10px solid #22BC66;
border-right: 18px solid #22BC66;
border-bottom: 10px solid #22BC66;
border-left: 18px solid #22BC66;
}
.button--red {
background-color: #FF6136;
border-top: 10px solid #FF6136;
border-right: 18px solid #FF6136;
border-bottom: 10px solid #FF6136;
border-left: 18px solid #FF6136;
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
text-align: center !important;
}
}
/* Attribute list ------------------------------ */
.attributes {
margin: 0 0 21px;
}
.attributes_content {
background-color: #F4F4F7;
padding: 16px;
}
.attributes_item {
padding: 0;
}
/* Related Items ------------------------------ */
.related {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.related_item {
padding: 10px 0;
color: #CBCCCF;
font-size: 15px;
line-height: 18px;
}
.related_item-title {
display: block;
margin: .5em 0 0;
}
.related_item-thumb {
display: block;
padding-bottom: 10px;
}
.related_heading {
border-top: 1px solid #CBCCCF;
text-align: center;
padding: 25px 0 10px;
}
/* Discount Code ------------------------------ */
.discount {
width: 100%;
margin: 0;
padding: 24px;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #F4F4F7;
border: 2px dashed #CBCCCF;
}
.discount_heading {
text-align: center;
}
.discount_body {
text-align: center;
font-size: 15px;
}
/* Social Icons ------------------------------ */
.social {
width: auto;
}
.social td {
padding: 0;
width: auto;
}
.social_icon {
height: 20px;
margin: 0 8px 10px 8px;
padding: 0;
}
/* Data table ------------------------------ */
.purchase {
width: 100%;
margin: 0;
padding: 35px 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_content {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_item {
padding: 10px 0;
color: #51545E;
font-size: 15px;
line-height: 18px;
}
.purchase_heading {
padding-bottom: 8px;
border-bottom: 1px solid #EAEAEC;
}
.purchase_heading p {
margin: 0;
color: #85878E;
font-size: 12px;
}
.purchase_footer {
padding-top: 15px;
border-top: 1px solid #EAEAEC;
}
.purchase_total {
margin: 0;
text-align: right;
font-weight: bold;
color: #333333;
}
.purchase_total--label {
padding: 0 15px 0 0;
}
body {
background-color: #F2F4F6;
color: #51545E;
}
p {
color: #51545E;
}
.email-wrapper {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #F2F4F6;
}
.email-content {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
/* Masthead ----------------------- */
.email-masthead {
padding: 25px 0;
text-align: center;
}
.email-masthead_logo {
width: 94px;
}
.email-masthead_name {
font-size: 16px;
font-weight: bold;
color: #A8AAAF;
text-decoration: none;
text-shadow: 0 1px 0 white;
}
/* Body ------------------------------ */
.email-body {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.email-body_inner {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #FFFFFF;
}
.email-footer {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.email-footer p {
color: #A8AAAF;
}
.body-action {
width: 100%;
margin: 30px auto;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #EAEAEC;
}
.content-cell {
padding: 45px;
}
/*Media Queries ------------------------------ */
@media only screen and (max-width: 600px) {
.email-body_inner,
.email-footer {
width: 100% !important;
}
}
@media (prefers-color-scheme: dark) {
body,
.email-body,
.email-body_inner,
.email-content,
.email-wrapper,
.email-masthead,
.email-footer {
background-color: #333333 !important;
color: #FFF !important;
}
p,
ul,
ol,
blockquote,
h1,
h2,
h3,
span,
.purchase_item {
color: #FFF !important;
}
.attributes_content,
.discount {
background-color: #222 !important;
}
.email-masthead_name {
text-shadow: none !important;
}
}
:root {
color-scheme: light dark;
supported-color-schemes: light dark;
}
</style>
<!--[if mso]>
<style type="text/css">
.f-fallback {
font-family: Arial, sans-serif;
}
</style>
<![endif]-->
<style type="text/css" rel="stylesheet" media="all">
body {
width: 100% !important;
height: 100%;
margin: 0;
-webkit-text-size-adjust: none;
}
body {
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
}
body {
background-color: #F2F4F6;
color: #51545E;
}
</style>
</head>
<body style="width: 100% !important; height: 100%; -webkit-text-size-adjust: none; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; background-color: #F2F4F6; color: #51545E; margin: 0;" bgcolor="#F2F4F6">
<span class="preheader" style="display: none !important; visibility: hidden; mso-hide: all; font-size: 1px; line-height: 1px; max-height: 0; max-width: 0; opacity: 0; overflow: hidden;">{# As I understand it, this hidden div is specifically meant to be shown in email clients' preview (preview of message content in list of emails) #}Reset your password</span>
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; background-color: #F2F4F6; margin: 0; padding: 0;" bgcolor="#F2F4F6">
<tr>
<td align="center" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px;">
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0; padding: 0;">
<tr>
<td class="email-masthead" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px; text-align: center; padding: 25px 0;" align="center">
<a href="{{ base_url }}" class="f-fallback email-masthead_name" style="color: #A8AAAF; font-size: 16px; font-weight: bold; text-decoration: none; text-shadow: 0 1px 0 white;">
{{ site_title }}
</a>
</td>
</tr>
{# Email Body #}
<tr>
<td class="email-body" width="570" cellpadding="0" cellspacing="0" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px; width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0; padding: 0;">
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation" style="width: 570px; -premailer-width: 570px; -premailer-cellpadding: 0; -premailer-cellspacing: 0; background-color: #FFFFFF; margin: 0 auto; padding: 0;" bgcolor="#FFFFFF">
{# Body content #}
<tr>
<td class="content-cell" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px; padding: 45px;">
<div class="f-fallback">
<h1 style="margin-top: 0; color: #333333; font-size: 22px; font-weight: bold; text-align: left;" align="left">Reset password</h1>
<p style="font-size: 16px; line-height: 1.625; color: #51545E; margin: 1.1875em 0 1.1875em;">
Someone (hopefully you) has requested to reset the password for the account associated with this email address.
</p>
{# Action #}
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; text-align: center; margin: 30px auto; padding: 0;">
<tr>
<td align="center" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px;">
{# Border based button https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design #}
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px;">
<a href="{{ reset_url }}" class="f-fallback button button--green" target="_blank" style="color: #51545E; background-color: #A5F3FC; display: inline-block; text-decoration: none; border-radius: 3px; box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16); -webkit-text-size-adjust: none; box-sizing: border-box; border-color: #A5F3FC; border-style: solid; border-width: 10px 18px;"><b>Reset password</b></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
{# Sub copy #}
<table class="body-sub" role="presentation" style="margin-top: 25px; padding-top: 25px; border-top-width: 1px; border-top-color: #EAEAEC; border-top-style: solid;">
<tr>
<td style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px;">
<p class="f-fallback sub" style="font-size: 13px; line-height: 1.625; color: #51545E; margin: .4em 0 0;">Copyable link:</p>
<p class="f-fallback sub" style="font-size: 13px; line-height: 1.625; color: #51545E; margin: 0 0 1.1875em;">{{ reset_url }}</p>
</td>
</tr>
</table>
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,5 @@
Someone (hopefully you) has requested to reset the password for the account associated with this email address.
If it was you, please follow the link below to reset your password.
{{ reset_url }}

View File

@@ -0,0 +1,21 @@
{% extends "barest_base.html" %}
{% load static %}
{% block title %}Password reset sent · {{ site_title }}{% endblock %}
{% block content %}
<div class="bg-cyan-100 h-screen overflow-y-scroll flex items-center justify-center"> {# the cyan background #}
<div class="bg-white lg:w-5/12 md:6/12 w-10/12"> {# the centered box #}
<div class="bg-slate-200 absolute left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full p-4 md:p-8"> {# the logo #}
<a href="/"><img src="{% static 'images/bugsink-logo.png' %}" class="h-8 w-8 md:h-16 md:w-16" alt="Bugsink"></a>
</div>
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">
A password reset link has been sent to your email address. Please check your inbox and follow the instructions to reset your password.
</div>
</div>
</div>
{% endblock %}

View File

@@ -8,9 +8,9 @@ from django.utils import timezone
from bugsink.app_settings import get_settings, CB_ANYBODY
from .forms import UserCreationForm, ResendConfirmationForm
from .forms import UserCreationForm, ResendConfirmationForm, RequestPasswordResetForm, SetPasswordForm
from .models import EmailVerification
from .tasks import send_confirm_email
from .tasks import send_confirm_email, send_reset_email
UserModel = get_user_model()
@@ -43,7 +43,7 @@ def signup(request):
return render(request, "signup.html", {"form": form})
def confirm_email(request, token):
def confirm_email(request, token=None):
# clean up expired tokens; doing this on every request is just fine, it saves us from having to run a cron job-like
EmailVerification.objects.filter(
created_at__lt=timezone.now() - timedelta(get_settings().USER_REGISTRATION_VERIFY_EMAIL_EXPIRY)).delete()
@@ -87,12 +87,73 @@ def resend_confirmation(request):
return render(request, "users/resend_confirmation.html", {"form": form})
def request_reset_password(request):
# something like this exists in Django too; copy-paste-modify from the other views was more simple than thoroughly
# understanding the Django implementation and hooking into it.
if request.method == 'POST':
form = RequestPasswordResetForm(request.POST)
if form.is_valid():
user = UserModel.objects.get(username=form.cleaned_data['email'])
# if not user.is_active no separate branch for this: password-reset implies email-confirmation
# we reuse the EmailVerification model for password resets; security wise it doesn't matter, because the
# visiting any link with the token implies control over the email account; and we have defined that such
# control implies both verification and password-resetting.
verification = EmailVerification.objects.create(user=user, email=user.username)
send_reset_email.delay(user.username, verification.token)
return render(request, "users/reset_password_email_sent.html", {"email": user.username})
else:
form = RequestPasswordResetForm()
return render(request, "users/request_reset_password.html", {"form": form})
def reset_password(request, token=None):
# clean up expired tokens; doing this on every request is just fine, it saves us from having to run a cron
# job-like thing
EmailVerification.objects.filter(
created_at__lt=timezone.now() - timedelta(get_settings().USER_REGISTRATION_VERIFY_EMAIL_EXPIRY)).delete()
try:
verification = EmailVerification.objects.get(token=token)
except EmailVerification.DoesNotExist:
# good enough (though a special page might be prettier)
raise Http404("Invalid or expired token")
user = verification.user
if request.method == 'POST':
form = SetPasswordForm(user, request.POST)
if form.is_valid():
user.is_active = True # password-reset implies email-confirmation
user.set_password(form.cleaned_data['new_password1'])
user.save()
verification.delete()
login(request, verification.user)
return redirect('home')
else:
form = SetPasswordForm(user)
return render(request, "users/reset_password.html", {"form": form})
DEBUG_CONTEXTS = {
"confirm_email": {
"site_title": get_settings().SITE_TITLE,
"base_url": get_settings().BASE_URL + "/",
"confirm_url": "http://example.com/confirm-email/1234567890abcdef", # nonsense to avoid circular import
},
"reset_password_email": {
"site_title": get_settings().SITE_TITLE,
"base_url": get_settings().BASE_URL + "/",
"reset_url": "http://example.com/reset-password/1234567890abcdef", # nonsense to avoid circular import
},
}