Password reset and change password flows
This commit is contained in:
parent
05b4cb59b0
commit
00a67f1b99
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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('/')
|
||||||
|
|
|
@ -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)
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue