Password recovery implemented

This commit is contained in:
Dirk Alders 2024-11-09 06:58:41 +01:00
parent 3a24ee2459
commit 12526f44d7
8 changed files with 151 additions and 24 deletions

View File

@ -1,26 +1,29 @@
from django.urls.base import reverse from django.urls.base import reverse
def url_login(request): def url_by_name(name, request, just_url=False):
nxt = request.GET.get('next', request.get_full_path()) base_url = reverse(name)
return reverse('users-login') + '?next=%s' % nxt if just_url:
return base_url
else:
nxt = request.GET.get('next', request.get_full_path())
return base_url + '?next=%s' % nxt
def url_logout(request): def url_login(request, just_url=False):
nxt = request.GET.get('next', request.get_full_path()) return url_by_name('users-login', request, just_url)
return reverse('users-logout') + '?next=%s' % nxt
def url_profile(request): def url_logout(request, just_url=False):
nxt = request.GET.get('next', request.get_full_path()) return url_by_name('users-logout', request, just_url)
return reverse('users-profile') + '?next=%s' % nxt
def url_register(request): def url_profile(request, just_url=False):
nxt = request.GET.get('next', request.get_full_path()) return url_by_name('users-profile', request, just_url)
return reverse('users-register') + '?next=%s' % nxt
def url_password_recovery(request): def url_recover(request, just_url=False):
nxt = request.GET.get('next', request.get_full_path()) return url_by_name('users-recover', request, just_url)
return reverse('users-password-recovery') + '?next=%s' % nxt
def url_register(request, just_url=False):
return url_by_name('users-register', request, just_url)

View File

@ -1,13 +1,14 @@
from django.urls.base import reverse from django.urls.base import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from themes import empty_entry_parameters, color_icon_url from themes import empty_entry_parameters, color_icon_url
from . import url_login, url_logout, url_register, url_profile from . import url_login, url_logout, url_register, url_profile, url_recover
from . import parameter from . import parameter
ADMIN_ENTRY_UID = 'admin-main' ADMIN_ENTRY_UID = 'admin-main'
LOGIN_ENTRY_UID = 'login-main' LOGIN_ENTRY_UID = 'login-main'
LOGOUT_ENTRY_UID = 'logout-main' LOGOUT_ENTRY_UID = 'logout-main'
PROFILE_ENTRY_UID = 'profile-main' PROFILE_ENTRY_UID = 'profile-main'
RECOVER_ENTRY_UID = 'recover-main'
REGISTER_ENTRY_UID = 'register-main' REGISTER_ENTRY_UID = 'register-main'
@ -32,6 +33,7 @@ def menubar(bar, request):
def actionbar(bar, request): def actionbar(bar, request):
bar.append_entry(*login_entry_parameters(request, left=True)) bar.append_entry(*login_entry_parameters(request, left=True))
bar.append_entry(*recover_entry_parameters(request, left=True))
if parameter.get(parameter.USERS_SELF_REGISTRATION): if parameter.get(parameter.USERS_SELF_REGISTRATION):
bar.append_entry(*register_entry_parameters(request, left=True)) bar.append_entry(*register_entry_parameters(request, left=True))
@ -43,10 +45,20 @@ def login_entry_parameters(request, left=False):
color_icon_url(request, 'login.png'), # icon color_icon_url(request, 'login.png'), # icon
url_login(request), # url url_login(request), # url
left, # left left, # left
False # active request.path == url_login(request, True) # active
) )
def recover_entry_parameters(request, left=False):
return (
RECOVER_ENTRY_UID, # uid
_('Recover'), # name
color_icon_url(request, 'recover.png'), # icon
url_recover(request), # url
left, # left
request.path == url_recover(request, True) # active
)
def register_entry_parameters(request, left=False): def register_entry_parameters(request, left=False):
return ( return (
REGISTER_ENTRY_UID, # uid REGISTER_ENTRY_UID, # uid
@ -54,7 +66,7 @@ def register_entry_parameters(request, left=False):
color_icon_url(request, 'register.png'), # icon color_icon_url(request, 'register.png'), # icon
url_register(request), # url url_register(request), # url
left, # left left, # left
False # active request.path == url_register(request, True) # active
) )
@ -87,5 +99,5 @@ def profile_entry_parameters(request):
color_icon_url(request, 'user.png'), # icon color_icon_url(request, 'user.png'), # icon
url_profile(request), # url url_profile(request), # url
False, # left False, # left
False # active request.path == url_profile(request, True) # active
) )

View File

@ -30,6 +30,12 @@ def send_validation_failed(uid, token):
mail_admins(subject, message, fail_silently=True) mail_admins(subject, message, fail_silently=True)
def send_recover_failed(uid, token):
subject = "Password recovery failed!"
message = f"uid = {uid}\ntoken = '{token}'"
mail_admins(subject, message, fail_silently=True)
def send_welcome_mail(new_user: User): def send_welcome_mail(new_user: User):
subject = "Welcome!" subject = "Welcome!"
message = """Hello %(username)s. message = """Hello %(username)s.
@ -56,3 +62,20 @@ def send_validation_mail(new_user, request):
[new_user.email], [new_user.email],
) )
send_mail(email_subject, message2, settings.EMAIL_FROM, [new_user.email], fail_silently=True) send_mail(email_subject, message2, settings.EMAIL_FROM, [new_user.email], fail_silently=True)
def send_recover_mail(user, request):
email_subject = "Password recovery"
message2 = render_to_string('users/email_recover.html', {
'name': user.first_name,
'base_url': request.build_absolute_uri('/')[:-1],
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'token': generate_token.make_token(user)
})
email = EmailMessage(
email_subject,
message2,
settings.EMAIL_HOST_USER,
[user.email],
)
send_mail(email_subject, message2, settings.EMAIL_FROM, [user.email], fail_silently=True)

View File

@ -1,11 +1,19 @@
from django import forms from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, PasswordChangeForm from django.contrib.auth.forms import AuthenticationForm, UserCreationForm, UserChangeForm, PasswordChangeForm, SetPasswordForm
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from .models import UserProfile, get_userprofile from .models import UserProfile, get_userprofile
from users import emails from users import emails
class PasswordRecoverForm(AuthenticationForm):
password = None
class PasswordRecoverChangeForm(SetPasswordForm):
fields = ["new_password1", "new_password2"]
class UserRegistrationForm(UserCreationForm): class UserRegistrationForm(UserCreationForm):
email = forms.EmailField() email = forms.EmailField()

View File

@ -0,0 +1,8 @@
{% autoescape off %}
Hello {{ name }},
you are able to recover your password by clicking on the following link.
Recovery Link: {{ base_url }}{% url 'users-recover-token' uidb64=uid token=token %}
{% endautoescape %}

View File

@ -0,0 +1,11 @@
{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
{% load i18n %}
{% block content %}
<form class="form" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="{% trans "Recover" %}" class="button" />
</form>
{% endblock content %}

View File

@ -4,9 +4,10 @@ from . import views
urlpatterns = [ urlpatterns = [
path('login', views.login, name='users-login'), path('login', views.login, name='users-login'),
path('logout', views.logout, name='users-logout'), path('logout', views.logout, name='users-logout'),
path('password_recovery', views.password_recovery, name='users-password-recovery'),
path('profile', views.profile, name='users-profile'), path('profile', views.profile, name='users-profile'),
path('register', views.register, name='users-register'), path('register', views.register, name='users-register'),
path('recover', views.recover, name='users-recover'),
path('recover-token/<uidb64>/<token>', views.recover_token, name='users-recover-token'),
path('validate/<uidb64>/<token>', views.validate, name='users-validate'), path('validate/<uidb64>/<token>', views.validate, name='users-validate'),
path('activate/<pk>', views.activate, name='users-activate'), path('activate/<pk>', views.activate, name='users-activate'),
] ]

View File

@ -11,7 +11,7 @@ from django.contrib.auth.models import User
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.http import urlsafe_base64_decode from django.utils.http import urlsafe_base64_decode
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from .forms import UserRegistrationForm, UserProfileForm, UserActivationForm, UserPasswordChangeForm from .forms import PasswordRecoverForm, UserRegistrationForm, UserProfileForm, UserActivationForm, UserPasswordChangeForm, PasswordRecoverChangeForm
import logging import logging
from .models import get_userprofile from .models import get_userprofile
from themes import Context from themes import Context
@ -59,6 +59,26 @@ def profile(request):
) )
return render(request, 'users/profile.html', context=context) return render(request, 'users/profile.html', context=context)
def recover(request):
context = Context(request) # needs to be executed first because of time mesurement
context_adaption(context, request, _('Password Recovery'))
if not request.POST:
form = PasswordRecoverForm(request)
else:
username = request.POST.get("username")
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
pass # hide non existing user (just do nothing)
else:
profile = get_userprofile(user)
if profile.mail_validated:
emails.send_recover_mail(user, request)
#
messages.info(request, _("If the user exists, you will get a reover email."))
return redirect("users-login")
context['form'] = form
return render(request, 'users/recover.html', context)
def register(request): def register(request):
context = Context(request) # needs to be executed first because of time mesurement context = Context(request) # needs to be executed first because of time mesurement
@ -116,10 +136,10 @@ def login(request):
if is_active: if is_active:
if parameter.get(parameter.USERS_SELF_REGISTRATION): if parameter.get(parameter.USERS_SELF_REGISTRATION):
messages.error(request, _('Login failed! You can do a password recorvery <a href="%(url_recover)s">here</a> or you can register <a href="%(url_register)s">here</a>.') % messages.error(request, _('Login failed! You can do a password recorvery <a href="%(url_recover)s">here</a> or you can register <a href="%(url_register)s">here</a>.') %
{'url_register': users.url_register(request), 'url_recover': users.url_password_recovery(request)}) {'url_register': users.url_register(request), 'url_recover': users.url_recover(request)})
else: else:
messages.error(request, _('Login failed! You can do a password recorvery <a href="%(url_recover)s">here</a>.') % messages.error(request, _('Login failed! You can do a password recorvery <a href="%(url_recover)s">here</a>.') %
{'url_recover': users.url_password_recovery(request)}) {'url_recover': users.url_recover(request)})
else: else:
messages.info(request, _("The account is deactivated. Confirm your email adress and wait for the administrator to activate your account.")) messages.info(request, _("The account is deactivated. Confirm your email adress and wait for the administrator to activate your account."))
@ -227,3 +247,44 @@ def activate(request, pk):
user_to_be_activated.delete() user_to_be_activated.delete()
messages.info(request, _("User deleted.")) messages.info(request, _("User deleted."))
return redirect("/") return redirect("/")
def recover_token(request, uidb64, token):
context = Context(request) # needs to be executed first because of time mesurement
print(settings.PASSWORD_RESET_TIMEOUT)
try:
uid = force_str(urlsafe_base64_decode(uidb64))
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
uid = None
myuser = None
else:
try:
myuser = User.objects.get(pk=uid)
except User.DoesNotExist:
myuser = None
if myuser is not None and generate_token.check_token(myuser, token):
if request.POST:
form = PasswordRecoverChangeForm(myuser, data=request.POST)
if form.is_valid():
form.save()
return redirect(request.GET.get('next') or 'users-login')
else:
form = PasswordRecoverChangeForm(myuser)
#
context_adaption(
context,
request,
_('Password recovery for %(username)s') % {'username': myuser.username},
form=form,
)
return render(request, 'users/recover.html', context=context)
else:
context_adaption(
context,
request,
_('Recovery failed'),
)
messages.info(request, _("Recovery failed. The system administrator will be informed."))
emails.send_recover_failed(uid, token)
return redirect("/")