mirror of
https://github.com/bugsink/bugsink.git
synced 2026-02-15 02:48:45 -06:00
Email verification
This commit is contained in:
@@ -23,6 +23,8 @@ DEFAULTS = {
|
||||
|
||||
# Users, teams, projects
|
||||
"USER_REGISTRATION": CB_ANYBODY, # who can register new users. default: anybody, i.e. users can register themselves
|
||||
"USER_REGISTRATION_VERIFY_EMAIL": True,
|
||||
"USER_REGISTRATION_VERIFY_EMAIL_EXPIRY": 3 * 24 * 60 * 60, # 7 days
|
||||
|
||||
# System inner workings:
|
||||
"DIGEST_IMMEDIATELY": True,
|
||||
|
||||
@@ -4,9 +4,10 @@ from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.contrib.auth import views as auth_views
|
||||
|
||||
from alerts.views import debug_email
|
||||
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
|
||||
from users.views import signup, confirm_email
|
||||
|
||||
from .views import home, trigger_error, favicon
|
||||
|
||||
@@ -20,6 +21,8 @@ urlpatterns = [
|
||||
path('', home, name='home'),
|
||||
|
||||
path("accounts/signup/", signup, name="signup"),
|
||||
path("accounts/confirm-email/<str:token>/", confirm_email, name="confirm_email"),
|
||||
|
||||
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"),
|
||||
|
||||
@@ -35,7 +38,8 @@ urlpatterns = [
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
path('debug-email-alerts/<str:template_name>/', debug_email),
|
||||
path('debug-alerts-email/<str:template_name>/', debug_alerts_email),
|
||||
path('debug-users-email/<str:template_name>/', debug_users_email),
|
||||
path('trigger-error/', trigger_error),
|
||||
path("__debug__/", include("debug_toolbar.urls")),
|
||||
]
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from .models import User
|
||||
from .models import User, EmailVerification
|
||||
|
||||
admin.site.register(User, UserAdmin)
|
||||
|
||||
|
||||
admin.site.register(EmailVerification)
|
||||
|
||||
26
users/migrations/0002_emailverification.py
Normal file
26
users/migrations/0002_emailverification.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 4.2.13 on 2024-05-29 15:00
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import secrets
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EmailVerification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('token', models.CharField(default=secrets.token_urlsafe, max_length=64)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,11 +1,24 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
import secrets
|
||||
|
||||
# > If you’re starting a new project, it’s highly recommended to set up a custom user model, even if the default User
|
||||
# > model is sufficient for you. This model behaves identically to the default user model, but you’ll be able to
|
||||
# > customize it in the future if the need arises
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
# > If you’re starting a new project, it’s highly recommended to set up a custom user model, even if the default
|
||||
# > User model is sufficient for you. This model behaves identically to the default user model, but you’ll be able
|
||||
# > to customize it in the future if the need arises
|
||||
|
||||
class Meta:
|
||||
db_table = 'auth_user'
|
||||
|
||||
|
||||
class EmailVerification(models.Model):
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
email = models.EmailField() # redundant, but future-proof for when we allow multiple emails per user
|
||||
token = models.CharField(max_length=64, default=secrets.token_urlsafe, blank=False, null=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user} ({self.email})"
|
||||
|
||||
21
users/tasks.py
Normal file
21
users/tasks.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from snappea.decorators import shared_task
|
||||
|
||||
from bugsink.app_settings import get_settings
|
||||
|
||||
from alerts.utils import send_rendered_email
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_confirm_email(email, token):
|
||||
send_rendered_email(
|
||||
subject="Confirm your email address",
|
||||
base_template_name="users/confirm_email",
|
||||
recipient_list=[email],
|
||||
context={
|
||||
"site_title": get_settings().SITE_TITLE,
|
||||
"base_url": get_settings().BASE_URL + "/",
|
||||
"confirm_url": reverse("confirm_email", kwargs={"token": token}),
|
||||
},
|
||||
)
|
||||
519
users/templates/users/confirm_email.html
Normal file
519
users/templates/users/confirm_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) #}Verify your email</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">Email verification</h1>
|
||||
|
||||
<p style="font-size: 16px; line-height: 1.625; color: #51545E; margin: 1.1875em 0 1.1875em;">
|
||||
Someone has registered a Bugsink account with this email address. If it was you, please click the button below to verify your 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="{{ confirm_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>Confirm email</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;">{{ confirm_url }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
5
users/templates/users/confirm_email.txt
Normal file
5
users/templates/users/confirm_email.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Someone has registered a Bugsink account with this email address.
|
||||
|
||||
If it was you, please follow the link below to verify your email address.
|
||||
|
||||
{{ confirm_url }}
|
||||
23
users/templates/users/confirm_email_sent.html
Normal file
23
users/templates/users/confirm_email_sent.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends "barest_base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Verification email 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 verification email has been sent to your email address. Please verify your email address to complete the registration process.
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
21
users/templates/users/email_confirmed.html
Normal file
21
users/templates/users/email_confirmed.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends "barest_base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Email verification successful · {{ 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">
|
||||
Email verification successful. You can now <a href="{% url 'login' %}"class="text-cyan-500">log in</a> to your account.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,11 +1,16 @@
|
||||
from django.contrib.auth import login # , authenticate
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth import login
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.http import Http404
|
||||
from django.utils import timezone
|
||||
|
||||
from bugsink.app_settings import get_settings, CB_ANYBODY
|
||||
|
||||
from .forms import UserCreationForm
|
||||
from .models import EmailVerification
|
||||
from .tasks import send_confirm_email
|
||||
|
||||
|
||||
UserModel = get_user_model()
|
||||
@@ -19,6 +24,16 @@ def signup(request):
|
||||
form = UserCreationForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
if get_settings().USER_REGISTRATION_VERIFY_EMAIL:
|
||||
user = form.save(commit=False)
|
||||
user.is_active = False
|
||||
user.save()
|
||||
|
||||
verification = EmailVerification.objects.create(user=user, email=user.username)
|
||||
send_confirm_email.delay(user.username, verification.token)
|
||||
|
||||
return render(request, "users/confirm_email_sent.html", {"email": user.username})
|
||||
|
||||
user = form.save()
|
||||
login(request, user)
|
||||
return redirect('home')
|
||||
@@ -26,3 +41,42 @@ def signup(request):
|
||||
form = UserCreationForm()
|
||||
|
||||
return render(request, "signup.html", {"form": form})
|
||||
|
||||
|
||||
def confirm_email(request, token):
|
||||
# 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()
|
||||
|
||||
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")
|
||||
|
||||
verification.user.is_active = True
|
||||
verification.user.save()
|
||||
verification.delete()
|
||||
|
||||
# I don't want to log the user in based on the verification email alone; although in principle doing so would not
|
||||
# be something fundamentally more insecure than what we do in the password-reset loop (in both cases access to the
|
||||
# email is enough to get access to Bugsink), better to err on the side of security.
|
||||
# If we ever want to introduce a more user-friendly approach, we could make automatic login dependent on some
|
||||
# (signed) cookie that's being set when registring. i.e.: if you've just recently entered your password in the same
|
||||
# browser, it works.
|
||||
# login(request, verification.user)
|
||||
|
||||
return render(request, "users/email_confirmed.html")
|
||||
|
||||
|
||||
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
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def debug_email(request, template_name):
|
||||
return render(request, 'users/' + template_name + ".html", DEBUG_CONTEXTS[template_name])
|
||||
|
||||
Reference in New Issue
Block a user