Browse Source

Password recovery implemented

master
Dirk Alders 1 month ago
parent
commit
12526f44d7
8 changed files with 151 additions and 24 deletions
  1. 18
    15
      __init__.py
  2. 16
    4
      context.py
  3. 23
    0
      emails.py
  4. 9
    1
      forms.py
  5. 8
    0
      templates/users/email_recover.html
  6. 11
    0
      templates/users/recover.html
  7. 2
    1
      urls.py
  8. 64
    3
      views.py

+ 18
- 15
__init__.py View File

1
 from django.urls.base import reverse
1
 from django.urls.base import reverse
2
 
2
 
3
 
3
 
4
-def url_login(request):
5
-    nxt = request.GET.get('next', request.get_full_path())
6
-    return reverse('users-login') + '?next=%s' % nxt
4
+def url_by_name(name, request, just_url=False):
5
+    base_url = reverse(name)
6
+    if just_url:
7
+        return base_url
8
+    else:
9
+        nxt = request.GET.get('next', request.get_full_path())
10
+        return base_url + '?next=%s' % nxt
7
 
11
 
8
 
12
 
9
-def url_logout(request):
10
-    nxt = request.GET.get('next', request.get_full_path())
11
-    return reverse('users-logout') + '?next=%s' % nxt
13
+def url_login(request, just_url=False):
14
+    return url_by_name('users-login', request, just_url)
12
 
15
 
13
 
16
 
14
-def url_profile(request):
15
-    nxt = request.GET.get('next', request.get_full_path())
16
-    return reverse('users-profile') + '?next=%s' % nxt
17
+def url_logout(request, just_url=False):
18
+    return url_by_name('users-logout', request, just_url)
17
 
19
 
18
 
20
 
19
-def url_register(request):
20
-    nxt = request.GET.get('next', request.get_full_path())
21
-    return reverse('users-register') + '?next=%s' % nxt
21
+def url_profile(request, just_url=False):
22
+    return url_by_name('users-profile', request, just_url)
22
 
23
 
23
 
24
 
24
-def url_password_recovery(request):
25
-    nxt = request.GET.get('next', request.get_full_path())
26
-    return reverse('users-password-recovery') + '?next=%s' % nxt
25
+def url_recover(request, just_url=False):
26
+    return url_by_name('users-recover', request, just_url)
27
+
28
+def url_register(request, just_url=False):
29
+    return url_by_name('users-register', request, just_url)

+ 16
- 4
context.py View File

1
 from django.urls.base import reverse
1
 from django.urls.base import reverse
2
 from django.utils.translation import gettext as _
2
 from django.utils.translation import gettext as _
3
 from themes import empty_entry_parameters, color_icon_url
3
 from themes import empty_entry_parameters, color_icon_url
4
-from . import url_login, url_logout, url_register, url_profile
4
+from . import url_login, url_logout, url_register, url_profile, url_recover
5
 from . import parameter
5
 from . import parameter
6
 
6
 
7
 ADMIN_ENTRY_UID = 'admin-main'
7
 ADMIN_ENTRY_UID = 'admin-main'
8
 LOGIN_ENTRY_UID = 'login-main'
8
 LOGIN_ENTRY_UID = 'login-main'
9
 LOGOUT_ENTRY_UID = 'logout-main'
9
 LOGOUT_ENTRY_UID = 'logout-main'
10
 PROFILE_ENTRY_UID = 'profile-main'
10
 PROFILE_ENTRY_UID = 'profile-main'
11
+RECOVER_ENTRY_UID = 'recover-main'
11
 REGISTER_ENTRY_UID = 'register-main'
12
 REGISTER_ENTRY_UID = 'register-main'
12
 
13
 
13
 
14
 
32
 
33
 
33
 def actionbar(bar, request):
34
 def actionbar(bar, request):
34
     bar.append_entry(*login_entry_parameters(request, left=True))
35
     bar.append_entry(*login_entry_parameters(request, left=True))
36
+    bar.append_entry(*recover_entry_parameters(request, left=True))
35
     if parameter.get(parameter.USERS_SELF_REGISTRATION):
37
     if parameter.get(parameter.USERS_SELF_REGISTRATION):
36
         bar.append_entry(*register_entry_parameters(request, left=True))
38
         bar.append_entry(*register_entry_parameters(request, left=True))
37
 
39
 
43
         color_icon_url(request, 'login.png'),       # icon
45
         color_icon_url(request, 'login.png'),       # icon
44
         url_login(request),                         # url
46
         url_login(request),                         # url
45
         left,                                       # left
47
         left,                                       # left
46
-        False                                       # active
48
+        request.path == url_login(request, True)    # active
47
     )
49
     )
48
 
50
 
49
 
51
 
52
+def recover_entry_parameters(request, left=False):
53
+    return (
54
+        RECOVER_ENTRY_UID,                          # uid
55
+        _('Recover'),                               # name
56
+        color_icon_url(request, 'recover.png'),     # icon
57
+        url_recover(request),                       # url
58
+        left,                                       # left
59
+        request.path == url_recover(request, True)  # active
60
+    )
61
+
50
 def register_entry_parameters(request, left=False):
62
 def register_entry_parameters(request, left=False):
51
     return (
63
     return (
52
         REGISTER_ENTRY_UID,                         # uid
64
         REGISTER_ENTRY_UID,                         # uid
54
         color_icon_url(request, 'register.png'),    # icon
66
         color_icon_url(request, 'register.png'),    # icon
55
         url_register(request),                      # url
67
         url_register(request),                      # url
56
         left,                                       # left
68
         left,                                       # left
57
-        False                                       # active
69
+        request.path == url_register(request, True) # active
58
     )
70
     )
59
 
71
 
60
 
72
 
87
         color_icon_url(request, 'user.png'),        # icon
99
         color_icon_url(request, 'user.png'),        # icon
88
         url_profile(request),                       # url
100
         url_profile(request),                       # url
89
         False,                                      # left
101
         False,                                      # left
90
-        False                                       # active
102
+        request.path == url_profile(request, True)  # active
91
     )
103
     )

+ 23
- 0
emails.py View File

30
     mail_admins(subject, message, fail_silently=True)
30
     mail_admins(subject, message, fail_silently=True)
31
 
31
 
32
 
32
 
33
+def send_recover_failed(uid, token):
34
+    subject = "Password recovery failed!"
35
+    message = f"uid = {uid}\ntoken = '{token}'"
36
+    mail_admins(subject, message, fail_silently=True)
37
+
38
+
33
 def send_welcome_mail(new_user: User):
39
 def send_welcome_mail(new_user: User):
34
     subject = "Welcome!"
40
     subject = "Welcome!"
35
     message = """Hello %(username)s.
41
     message = """Hello %(username)s.
56
         [new_user.email],
62
         [new_user.email],
57
     )
63
     )
58
     send_mail(email_subject, message2, settings.EMAIL_FROM, [new_user.email], fail_silently=True)
64
     send_mail(email_subject, message2, settings.EMAIL_FROM, [new_user.email], fail_silently=True)
65
+
66
+
67
+def send_recover_mail(user, request):
68
+    email_subject = "Password recovery"
69
+    message2 = render_to_string('users/email_recover.html', {
70
+        'name': user.first_name,
71
+        'base_url': request.build_absolute_uri('/')[:-1],
72
+        'uid': urlsafe_base64_encode(force_bytes(user.pk)),
73
+        'token': generate_token.make_token(user)
74
+    })
75
+    email = EmailMessage(
76
+        email_subject,
77
+        message2,
78
+        settings.EMAIL_HOST_USER,
79
+        [user.email],
80
+    )
81
+    send_mail(email_subject, message2, settings.EMAIL_FROM, [user.email], fail_silently=True)

+ 9
- 1
forms.py View File

1
 from django import forms
1
 from django import forms
2
 from django.contrib.auth.models import User
2
 from django.contrib.auth.models import User
3
-from django.contrib.auth.forms import UserCreationForm, UserChangeForm, PasswordChangeForm
3
+from django.contrib.auth.forms import AuthenticationForm, UserCreationForm, UserChangeForm, PasswordChangeForm, SetPasswordForm
4
 from django.utils.translation import gettext as _
4
 from django.utils.translation import gettext as _
5
 from .models import UserProfile, get_userprofile
5
 from .models import UserProfile, get_userprofile
6
 from users import emails
6
 from users import emails
7
 
7
 
8
 
8
 
9
+class PasswordRecoverForm(AuthenticationForm):
10
+    password = None
11
+
12
+
13
+class PasswordRecoverChangeForm(SetPasswordForm):
14
+    fields = ["new_password1", "new_password2"]
15
+
16
+
9
 class UserRegistrationForm(UserCreationForm):
17
 class UserRegistrationForm(UserCreationForm):
10
     email = forms.EmailField()
18
     email = forms.EmailField()
11
 
19
 

+ 8
- 0
templates/users/email_recover.html View File

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

+ 11
- 0
templates/users/recover.html View File

1
+{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
2
+{% load i18n %}
3
+
4
+{% block content %}
5
+
6
+    <form class="form" method="post">
7
+      {% csrf_token %}
8
+      {{ form.as_p }}
9
+      <input type="submit" value="{% trans "Recover" %}" class="button" />
10
+    </form>
11
+{% endblock content %}

+ 2
- 1
urls.py View File

4
 urlpatterns = [
4
 urlpatterns = [
5
     path('login', views.login, name='users-login'),
5
     path('login', views.login, name='users-login'),
6
     path('logout', views.logout, name='users-logout'),
6
     path('logout', views.logout, name='users-logout'),
7
-    path('password_recovery', views.password_recovery, name='users-password-recovery'),
8
     path('profile', views.profile, name='users-profile'),
7
     path('profile', views.profile, name='users-profile'),
9
     path('register', views.register, name='users-register'),
8
     path('register', views.register, name='users-register'),
9
+    path('recover', views.recover, name='users-recover'),
10
+    path('recover-token/<uidb64>/<token>', views.recover_token, name='users-recover-token'),
10
     path('validate/<uidb64>/<token>', views.validate, name='users-validate'),
11
     path('validate/<uidb64>/<token>', views.validate, name='users-validate'),
11
     path('activate/<pk>', views.activate, name='users-activate'),
12
     path('activate/<pk>', views.activate, name='users-activate'),
12
 ]
13
 ]

+ 64
- 3
views.py View File

11
 from django.utils.encoding import force_str
11
 from django.utils.encoding import force_str
12
 from django.utils.http import urlsafe_base64_decode
12
 from django.utils.http import urlsafe_base64_decode
13
 from django.utils.translation import gettext as _
13
 from django.utils.translation import gettext as _
14
-from .forms import UserRegistrationForm, UserProfileForm, UserActivationForm, UserPasswordChangeForm
14
+from .forms import PasswordRecoverForm, UserRegistrationForm, UserProfileForm, UserActivationForm, UserPasswordChangeForm, PasswordRecoverChangeForm
15
 import logging
15
 import logging
16
 from .models import get_userprofile
16
 from .models import get_userprofile
17
 from themes import Context
17
 from themes import Context
59
     )
59
     )
60
     return render(request, 'users/profile.html', context=context)
60
     return render(request, 'users/profile.html', context=context)
61
 
61
 
62
+def recover(request):
63
+    context = Context(request)      # needs to be executed first because of time mesurement
64
+    context_adaption(context, request, _('Password Recovery'))
65
+    if not request.POST:
66
+        form = PasswordRecoverForm(request)
67
+    else:
68
+        username = request.POST.get("username")
69
+        try:
70
+            user = User.objects.get(username=username)
71
+        except User.DoesNotExist:
72
+            pass # hide non existing user (just do nothing)
73
+        else:
74
+            profile = get_userprofile(user)
75
+            if profile.mail_validated:
76
+                emails.send_recover_mail(user, request)
77
+        #
78
+        messages.info(request, _("If the user exists, you will get a reover email."))
79
+        return redirect("users-login")
80
+    context['form'] = form
81
+    return render(request, 'users/recover.html', context)
62
 
82
 
63
 def register(request):
83
 def register(request):
64
     context = Context(request)      # needs to be executed first because of time mesurement
84
     context = Context(request)      # needs to be executed first because of time mesurement
116
             if is_active:
136
             if is_active:
117
                 if parameter.get(parameter.USERS_SELF_REGISTRATION):
137
                 if parameter.get(parameter.USERS_SELF_REGISTRATION):
118
                     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>.') %
138
                     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>.') %
119
-                                   {'url_register': users.url_register(request), 'url_recover': users.url_password_recovery(request)})
139
+                                   {'url_register': users.url_register(request), 'url_recover': users.url_recover(request)})
120
                 else:
140
                 else:
121
                     messages.error(request, _('Login failed! You can do a password recorvery <a href="%(url_recover)s">here</a>.') %
141
                     messages.error(request, _('Login failed! You can do a password recorvery <a href="%(url_recover)s">here</a>.') %
122
-                                   {'url_recover': users.url_password_recovery(request)})
142
+                                   {'url_recover': users.url_recover(request)})
123
             else:
143
             else:
124
                 messages.info(request, _("The account is deactivated. Confirm your email adress and wait for the administrator to activate your account."))
144
                 messages.info(request, _("The account is deactivated. Confirm your email adress and wait for the administrator to activate your account."))
125
 
145
 
227
             user_to_be_activated.delete()
247
             user_to_be_activated.delete()
228
             messages.info(request, _("User deleted."))
248
             messages.info(request, _("User deleted."))
229
     return redirect("/")
249
     return redirect("/")
250
+
251
+
252
+def recover_token(request, uidb64, token):
253
+    context = Context(request)      # needs to be executed first because of time mesurement
254
+    print(settings.PASSWORD_RESET_TIMEOUT)
255
+    try:
256
+        uid = force_str(urlsafe_base64_decode(uidb64))
257
+    except (TypeError, ValueError, OverflowError, User.DoesNotExist):
258
+        uid = None
259
+        myuser = None
260
+    else:
261
+        try:
262
+            myuser = User.objects.get(pk=uid)
263
+        except User.DoesNotExist:
264
+            myuser = None
265
+
266
+    if myuser is not None and generate_token.check_token(myuser, token):
267
+        if request.POST:
268
+            form = PasswordRecoverChangeForm(myuser, data=request.POST)
269
+            if form.is_valid():
270
+                form.save()
271
+                return redirect(request.GET.get('next') or 'users-login')
272
+        else:
273
+            form = PasswordRecoverChangeForm(myuser)
274
+        #
275
+        context_adaption(
276
+            context,
277
+            request,
278
+            _('Password recovery for %(username)s') % {'username': myuser.username},
279
+            form=form,
280
+        )
281
+        return render(request, 'users/recover.html', context=context)
282
+    else:
283
+        context_adaption(
284
+            context,
285
+            request,
286
+            _('Recovery failed'),
287
+        )
288
+        messages.info(request, _("Recovery failed. The system administrator will be informed."))
289
+        emails.send_recover_failed(uid, token)
290
+        return redirect("/")

Loading…
Cancel
Save