Password reset and change password flows

This commit is contained in:
Mouse Reeve 2021-01-12 08:48:31 -08:00
parent 05b4cb59b0
commit 00a67f1b99
8 changed files with 130 additions and 100 deletions

View File

@ -8,9 +8,8 @@
{% for error in errors %} {% for error in errors %}
<p class="is-danger">{{ error }}</p> <p class="is-danger">{{ error }}</p>
{% endfor %} {% endfor %}
<form name="reset-password" method="post" action="/reset-password"> <form name="password-reset" method="post" action="/password-reset/{{ code }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="reset-code" value="{{ code }}">
<div class="field"> <div class="field">
<label class="label" for="id_password">Password:</label> <label class="label" for="id_password">Password:</label>
<div class="control"> <div class="control">

View File

@ -7,7 +7,7 @@
<h1 class="title">Reset Password</h1> <h1 class="title">Reset Password</h1>
{% if message %}<p>{{ message }}</p>{% endif %} {% if message %}<p>{{ message }}</p>{% endif %}
<p>A link to reset your password will be sent to your email address</p> <p>A link to reset your password will be sent to your email address</p>
<form name="reset-password" method="post" action="/reset-password-request"> <form name="password-reset" method="post" action="/password-reset">
{% csrf_token %} {% csrf_token %}
<div class="field"> <div class="field">
<label class="label" for="id_email_register">Email address:</label> <label class="label" for="id_email_register">Email address:</label>

View File

@ -43,12 +43,16 @@ urlpatterns = [
# TODO: robots.txt # TODO: robots.txt
# authentication # authentication
re_path(r'^login/?$', views.LoginView.as_view()), re_path(r'^login/?$', views.Login.as_view()),
re_path(r'^register/?$', views.RegisterView.as_view()), re_path(r'^register/?$', views.Register.as_view()),
re_path(r'^logout/?$', views.Logout.as_view()),
re_path(r'^password-reset/?$', views.PasswordResetRequest.as_view()),
re_path(r'^password-reset/(?P<code>[A-Za-z0-9]+)/?$',
views.PasswordReset.as_view()),
re_path(r'^change-password/?$', views.ChangePassword),
re_path(r'^about/?$', vviews.about_page), re_path(r'^about/?$', vviews.about_page),
re_path(r'^password-reset/?$', vviews.password_reset_request),
re_path(r'^password-reset/(?P<code>[A-Za-z0-9]+)/?$', vviews.password_reset),
re_path(r'^invite/?$', vviews.manage_invites), re_path(r'^invite/?$', vviews.manage_invites),
re_path(r'^invite/(?P<code>[A-Za-z0-9]+)/?$', vviews.invite_page), re_path(r'^invite/(?P<code>[A-Za-z0-9]+)/?$', vviews.invite_page),
@ -92,11 +96,6 @@ urlpatterns = [
re_path(r'^search/?$', vviews.search), re_path(r'^search/?$', vviews.search),
# internal action endpoints # internal action endpoints
re_path(r'^logout/?$', actions.user_logout),
re_path(r'^reset-password-request/?$', actions.password_reset_request),
re_path(r'^reset-password/?$', actions.password_reset),
re_path(r'^change-password/?$', actions.password_change),
re_path(r'^edit-profile/?$', actions.edit_profile), re_path(r'^edit-profile/?$', actions.edit_profile),
re_path(r'^import-data/?$', actions.import_data), re_path(r'^import-data/?$', actions.import_data),

View File

@ -6,95 +6,21 @@ from PIL import Image
import dateutil.parser import dateutil.parser
from dateutil.parser import ParserError from dateutil.parser import ParserError
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.core.exceptions import PermissionDenied
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import transaction from django.db import transaction
from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils import timezone from django.utils import timezone
from django.views.decorators.http import require_GET, require_POST from django.views.decorators.http import require_POST
from bookwyrm import forms, models, outgoing, goodreads_import from bookwyrm import forms, models, outgoing, goodreads_import
from bookwyrm.connectors import connector_manager from bookwyrm.connectors import connector_manager
from bookwyrm.broadcast import broadcast from bookwyrm.broadcast import broadcast
from bookwyrm.emailing import password_reset_email
from bookwyrm.settings import DOMAIN
from bookwyrm.vviews import get_user_from_username, get_edition from bookwyrm.vviews import get_user_from_username, get_edition
@login_required
@require_GET
def user_logout(request):
''' done with this place! outa here! '''
logout(request)
return redirect('/')
@require_POST
def password_reset_request(request):
''' create a password reset token '''
email = request.POST.get('email')
try:
user = models.User.objects.get(email=email)
except models.User.DoesNotExist:
return redirect('/password-reset')
# remove any existing password reset cods for this user
models.PasswordReset.objects.filter(user=user).all().delete()
# create a new reset code
code = models.PasswordReset.objects.create(user=user)
password_reset_email(code)
data = {'message': 'Password reset link sent to %s' % email}
return TemplateResponse(request, 'password_reset_request.html', data)
@require_POST
def password_reset(request):
''' allow a user to change their password through an emailed token '''
try:
reset_code = models.PasswordReset.objects.get(
code=request.POST.get('reset-code')
)
except models.PasswordReset.DoesNotExist:
data = {'errors': ['Invalid password reset link']}
return TemplateResponse(request, 'password_reset.html', data)
user = reset_code.user
new_password = request.POST.get('password')
confirm_password = request.POST.get('confirm-password')
if new_password != confirm_password:
data = {'errors': ['Passwords do not match']}
return TemplateResponse(request, 'password_reset.html', data)
user.set_password(new_password)
user.save()
login(request, user)
reset_code.delete()
return redirect('/')
@login_required
@require_POST
def password_change(request):
''' allow a user to change their password '''
new_password = request.POST.get('password')
confirm_password = request.POST.get('confirm-password')
if new_password != confirm_password:
return redirect('/user-edit')
request.user.set_password(new_password)
request.user.save()
login(request, request.user)
return redirect('/user/%s' % request.user.localname)
@login_required @login_required
@require_POST @require_POST
def edit_profile(request): def edit_profile(request):

View File

@ -1,2 +1,3 @@
''' make sure all our nice views are available ''' ''' make sure all our nice views are available '''
from .authentication import LoginView, RegisterView from .authentication import Login, Register, Logout
from .password import PasswordResetRequest, PasswordReset, ChangePassword

View File

@ -1,9 +1,11 @@
''' class views for login/register/password management views ''' ''' class views for login/register views '''
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from bookwyrm import forms, models from bookwyrm import forms, models
@ -11,7 +13,7 @@ from bookwyrm.settings import DOMAIN
# pylint: disable= no-self-use # pylint: disable= no-self-use
class LoginView(View): class Login(View):
''' authenticate an existing user ''' ''' authenticate an existing user '''
def get(self, request): def get(self, request):
''' login page ''' ''' login page '''
@ -49,7 +51,7 @@ class LoginView(View):
return TemplateResponse(request, 'login.html', data) return TemplateResponse(request, 'login.html', data)
class RegisterView(View): class Register(View):
''' register a user ''' ''' register a user '''
def post(self, request): def post(self, request):
''' join the server ''' ''' join the server '''
@ -100,3 +102,12 @@ class RegisterView(View):
login(request, user) login(request, user)
return redirect('/') return redirect('/')
@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('/')

102
bookwyrm/views/password.py Normal file
View File

@ -0,0 +1,102 @@
''' class views for password management '''
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import models
from bookwyrm.emailing import password_reset_email
# pylint: disable= no-self-use
class PasswordResetRequest(View):
''' forgot password flow '''
def get(self, request):
''' password reset page '''
return TemplateResponse(
request,
'password_reset_request.html',
{'title': 'Reset Password'}
)
def post(self, request):
''' create a password reset token '''
email = request.POST.get('email')
try:
user = models.User.objects.get(email=email)
except models.User.DoesNotExist:
return redirect('/password-reset')
# remove any existing password reset cods for this user
models.PasswordReset.objects.filter(user=user).all().delete()
# create a new reset code
code = models.PasswordReset.objects.create(user=user)
password_reset_email(code)
data = {'message': 'Password reset link sent to %s' % email}
return TemplateResponse(request, 'password_reset_request.html', data)
class PasswordReset(View):
''' set new password '''
def get(self, request, code):
''' endpoint for sending invites '''
if request.user.is_authenticated:
return redirect('/')
try:
reset_code = models.PasswordReset.objects.get(code=code)
if not reset_code.valid():
raise PermissionDenied
except models.PasswordReset.DoesNotExist:
raise PermissionDenied
return TemplateResponse(
request,
'password_reset.html',
{'title': 'Reset Password', 'code': reset_code.code}
)
def post(self, request, code):
''' allow a user to change their password through an emailed token '''
try:
reset_code = models.PasswordReset.objects.get(
code=code
)
except models.PasswordReset.DoesNotExist:
data = {'errors': ['Invalid password reset link']}
return TemplateResponse(request, 'password_reset.html', data)
user = reset_code.user
new_password = request.POST.get('password')
confirm_password = request.POST.get('confirm-password')
if new_password != confirm_password:
data = {'errors': ['Passwords do not match']}
return TemplateResponse(request, 'password_reset.html', data)
user.set_password(new_password)
user.save()
login(request, user)
reset_code.delete()
return redirect('/')
@method_decorator(login_required, name='dispatch')
class ChangePassword(View):
''' change password as logged in user '''
def post(self, request):
''' allow a user to change their password '''
new_password = request.POST.get('password')
confirm_password = request.POST.get('confirm-password')
if new_password != confirm_password:
return redirect('/user-edit')
request.user.set_password(new_password)
request.user.save()
login(request, request.user)
return redirect('/user/%s' % request.user.localname)

View File

@ -332,14 +332,6 @@ def about_page(request):
return TemplateResponse(request, 'about.html', data) return TemplateResponse(request, 'about.html', data)
@require_GET
def password_reset_request(request):
''' invite management page '''
return TemplateResponse(
request,
'password_reset_request.html',
{'title': 'Reset Password'}
)
@require_GET @require_GET