Confirm email views

This commit is contained in:
Mouse Reeve 2021-08-06 16:24:57 -07:00
parent 247a7f7489
commit 5926224d7e
11 changed files with 78 additions and 28 deletions

View file

@ -36,9 +36,6 @@ FLOWER_PORT=8888
#FLOWER_USER=mouse #FLOWER_USER=mouse
#FLOWER_PASSWORD=changeme #FLOWER_PASSWORD=changeme
# make users confirm their email addresses after registration
CONFIRM_EMAIL=false
EMAIL_HOST="smtp.mailgun.org" EMAIL_HOST="smtp.mailgun.org"
EMAIL_PORT=587 EMAIL_PORT=587
EMAIL_HOST_USER=mail@your.domain.here EMAIL_HOST_USER=mail@your.domain.here

View file

@ -36,9 +36,6 @@ FLOWER_PORT=8888
FLOWER_USER=mouse FLOWER_USER=mouse
FLOWER_PASSWORD=changeme FLOWER_PASSWORD=changeme
# make users confirm their email addresses after registration
CONFIRM_EMAIL=true
EMAIL_HOST="smtp.mailgun.org" EMAIL_HOST="smtp.mailgun.org"
EMAIL_PORT=587 EMAIL_PORT=587
EMAIL_HOST_USER=mail@your.domain.here EMAIL_HOST_USER=mail@your.domain.here

View file

@ -30,6 +30,7 @@ def email_confirmation_email(user):
data["confirmation_link"] = user.confirmation_link data["confirmation_link"] = user.confirmation_link
send_email.delay(user.email, *format_email("confirm_email", data)) send_email.delay(user.email, *format_email("confirm_email", data))
def invite_email(invite_request): def invite_email(invite_request):
"""send out an invite code""" """send out an invite code"""
data = email_data() data = email_data()

View file

@ -1,4 +1,4 @@
# Generated by Django 3.2.4 on 2021-08-06 21:41 # Generated by Django 3.2.4 on 2021-08-06 23:24
import bookwyrm.models.base_model import bookwyrm.models.base_model
from django.db import migrations, models from django.db import migrations, models
@ -11,6 +11,11 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.AddField(
model_name="sitesettings",
name="require_confirm_email",
field=models.BooleanField(default=True),
),
migrations.AddField( migrations.AddField(
model_name="user", model_name="user",
name="confirmation_code", name="confirmation_code",

View file

@ -31,6 +31,7 @@ class SiteSettings(models.Model):
# registration # registration
allow_registration = models.BooleanField(default=True) allow_registration = models.BooleanField(default=True)
allow_invite_requests = models.BooleanField(default=True) allow_invite_requests = models.BooleanField(default=True)
require_confirm_email = models.BooleanField(default=True)
# images # images
logo = models.ImageField(upload_to="logos/", null=True, blank=True) logo = models.ImageField(upload_to="logos/", null=True, blank=True)

View file

@ -26,11 +26,13 @@ from .base_model import BookWyrmModel, DeactivationReason, new_access_code
from .federated_server import FederatedServer from .federated_server import FederatedServer
from . import fields, Review from . import fields, Review
def site_link(): def site_link():
"""helper for generating links to the site""" """helper for generating links to the site"""
protocol = "https" if USE_HTTPS else "http" protocol = "https" if USE_HTTPS else "http"
return f"{protocol}://{DOMAIN}" return f"{protocol}://{DOMAIN}"
class User(OrderedCollectionPageMixin, AbstractUser): class User(OrderedCollectionPageMixin, AbstractUser):
"""a user who wants to read books""" """a user who wants to read books"""
@ -218,7 +220,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
self.following.order_by("-updated_date").all(), self.following.order_by("-updated_date").all(),
remote_id=remote_id, remote_id=remote_id,
id_only=True, id_only=True,
**kwargs **kwargs,
) )
def to_followers_activity(self, **kwargs): def to_followers_activity(self, **kwargs):
@ -228,7 +230,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
self.followers.order_by("-updated_date").all(), self.followers.order_by("-updated_date").all(),
remote_id=remote_id, remote_id=remote_id,
id_only=True, id_only=True,
**kwargs **kwargs,
) )
def to_activity(self, **kwargs): def to_activity(self, **kwargs):

View file

@ -24,9 +24,6 @@ CELERY_ACCEPT_CONTENT = ["application/json"]
CELERY_TASK_SERIALIZER = "json" CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json" CELERY_RESULT_SERIALIZER = "json"
# make users confirm their email addresses after registration
CONFIRM_EMAIL = env("CONFIRM_EMAIL", True)
# email # email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend") EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
EMAIL_HOST = env("EMAIL_HOST") EMAIL_HOST = env("EMAIL_HOST")

View file

@ -0,0 +1 @@
{% extends "layout.html" %}

View file

@ -46,7 +46,14 @@ urlpatterns = [
re_path("^api/updates/stream/(?P<stream>[a-z]+)/?$", views.get_unread_status_count), re_path("^api/updates/stream/(?P<stream>[a-z]+)/?$", views.get_unread_status_count),
# authentication # authentication
re_path(r"^login/?$", views.Login.as_view(), name="login"), re_path(r"^login/?$", views.Login.as_view(), name="login"),
re_path(r"^login/(?P<confirmed>confirmed)?$", views.Login.as_view(), name="login"),
re_path(r"^register/?$", views.Register.as_view()), re_path(r"^register/?$", views.Register.as_view()),
re_path(r"confirm-email/?$", views.ConfirmEmail.as_view(), name="confirm-email"),
re_path(
r"confirm-email/(?P<code>[A-Za-z0-9]+)/?$",
views.ConfirmEmailCode.as_view(),
name="confirm-email-code",
),
re_path(r"^logout/?$", views.Logout.as_view(), name="logout"), re_path(r"^logout/?$", views.Logout.as_view(), name="logout"),
re_path( re_path(
r"^password-reset/?$", r"^password-reset/?$",

View file

@ -1,6 +1,7 @@
""" make sure all our nice views are available """ """ make sure all our nice views are available """
from .announcements import Announcements, Announcement, delete_announcement from .announcements import Announcements, Announcement, delete_announcement
from .authentication import Login, Register, Logout from .authentication import Login, Register, Logout
from .authentication import ConfirmEmail, ConfirmEmailCode
from .author import Author, EditAuthor from .author import Author, EditAuthor
from .block import Block, unblock from .block import Block, unblock
from .books import Book, EditBook, ConfirmEditBook, Editions from .books import Book, EditBook, ConfirmEditBook, Editions

View file

@ -10,20 +10,21 @@ from django.views.decorators.csrf import csrf_exempt
from django.views import View from django.views import View
from bookwyrm import emailing, forms, models from bookwyrm import emailing, forms, models
from bookwyrm.settings import DOMAIN, CONFIRM_EMAIL from bookwyrm.settings import DOMAIN
# pylint: disable= no-self-use # pylint: disable=no-self-use
@method_decorator(csrf_exempt, name="dispatch") @method_decorator(csrf_exempt, name="dispatch")
class Login(View): class Login(View):
"""authenticate an existing user""" """authenticate an existing user"""
def get(self, request): def get(self, request, confirmed=None):
"""login page""" """login page"""
if request.user.is_authenticated: if request.user.is_authenticated:
return redirect("/") return redirect("/")
# sene user to the login page # send user to the login page
data = { data = {
"show_confirmed_email": confirmed,
"login_form": forms.LoginForm(), "login_form": forms.LoginForm(),
"register_form": forms.RegisterForm(), "register_form": forms.RegisterForm(),
} }
@ -37,14 +38,15 @@ class Login(View):
localname = login_form.data["localname"] localname = login_form.data["localname"]
if "@" in localname: # looks like an email address to me if "@" in localname: # looks like an email address to me
email = localname
try: try:
username = models.User.objects.get(email=email) username = models.User.objects.get(email=localname).username
except models.User.DoesNotExist: # maybe it's a full username? except models.User.DoesNotExist: # maybe it's a full username?
username = localname username = localname
else: else:
username = "%s@%s" % (localname, DOMAIN) username = "%s@%s" % (localname, DOMAIN)
password = login_form.data["password"] password = login_form.data["password"]
# perform authentication
user = authenticate(request, username=username, password=password) user = authenticate(request, username=username, password=password)
if user is not None: if user is not None:
# successful login # successful login
@ -53,6 +55,12 @@ class Login(View):
user.save(broadcast=False, update_fields=["last_active_date"]) user.save(broadcast=False, update_fields=["last_active_date"])
return redirect(request.GET.get("next", "/")) return redirect(request.GET.get("next", "/"))
# maybe the user is pending email confirmation
if models.User.objects.filter(
username=username, is_active=False, deactivation_reason="pending"
).exists():
return redirect("confirm-email")
# login errors # login errors
login_form.non_field_errors = "Username or password are incorrect" login_form.non_field_errors = "Username or password are incorrect"
register_form = forms.RegisterForm() register_form = forms.RegisterForm()
@ -60,12 +68,23 @@ class Login(View):
return TemplateResponse(request, "login.html", data) return TemplateResponse(request, "login.html", data)
@method_decorator(login_required, name="dispatch")
class Logout(View):
"""log out"""
def get(self, request):
"""done with this place! outa here!"""
logout(request)
return redirect("/")
class Register(View): class Register(View):
"""register a user""" """register a user"""
def post(self, request): def post(self, request):
"""join the server""" """join the server"""
if not models.SiteSettings.get().allow_registration: settings = models.SiteSettings.get()
if not settings.allow_registration:
invite_code = request.POST.get("invite_code") invite_code = request.POST.get("invite_code")
if not invite_code: if not invite_code:
@ -109,14 +128,15 @@ class Register(View):
password, password,
localname=localname, localname=localname,
local=True, local=True,
is_active=not CONFIRM_EMAIL, deactivation_reason="pending" if settings.require_confirm_email else None,
is_active=not settings.require_confirm_email,
) )
if invite: if invite:
invite.times_used += 1 invite.times_used += 1
invite.invitees.add(user) invite.invitees.add(user)
invite.save() invite.save()
if CONFIRM_EMAIL: if settings.require_confirm_email:
emailing.email_confirmation_email(user) emailing.email_confirmation_email(user)
return redirect("confirm-email") return redirect("confirm-email")
@ -124,11 +144,32 @@ class Register(View):
return redirect("get-started-profile") return redirect("get-started-profile")
@method_decorator(login_required, name="dispatch") class ConfirmEmail(View):
class Logout(View): """enter code to confirm email address"""
"""log out"""
def get(self, request): def get(self, request): # pylint: disable=unused-argument
"""done with this place! outa here!""" """you need a code! keep looking"""
logout(request) settings = models.SiteSettings.get()
return redirect("/") if request.user.is_authenticated or not settings.require_confirm_email:
return redirect("/")
return TemplateResponse(request, "confirm_email.html")
class ConfirmEmailCode(View):
"""confirm email address"""
def get(self, request, code): # pylint: disable=unused-argument
"""you got the code! good work"""
settings = models.SiteSettings.get()
if request.user.is_authenticated or not settings.require_confirm_email:
return redirect("/")
# look up the user associated with this code
user = get_object_or_404(models.User, confirmation_code=code)
# update the user
user.is_active = True
user.deactivation_reason = None
user.save(broadcast=False, update_fields=["is_active", "deactivation_reason"])
# direct the user to log in
return redirect("login", confirmed="confirmed")