mirror of
https://github.com/bugsink/bugsink.git
synced 2026-01-04 20:30:30 -06:00
Password reset
This commit is contained in:
@@ -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"),
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}),
|
||||
},
|
||||
)
|
||||
|
||||
41
users/templates/users/request_reset_password.html
Normal file
41
users/templates/users/request_reset_password.html
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
53
users/templates/users/reset_password.html
Normal file
53
users/templates/users/reset_password.html
Normal 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 %}
|
||||
519
users/templates/users/reset_password_email.html
Normal file
519
users/templates/users/reset_password_email.html
Normal 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&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: "Nunito Sans", 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: "Nunito Sans", 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: "Nunito Sans", 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: "Nunito Sans", 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: "Nunito Sans", 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: "Nunito Sans", 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: "Nunito Sans", 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: "Nunito Sans", 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>
|
||||
5
users/templates/users/reset_password_email.txt
Normal file
5
users/templates/users/reset_password_email.txt
Normal 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 }}
|
||||
21
users/templates/users/reset_password_email_sent.html
Normal file
21
users/templates/users/reset_password_email_sent.html
Normal 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 %}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user