Переглянути джерело

Self registration with mail validation and admin activation implemented

master
Dirk Alders 5 дні тому
джерело
коміт
d18f439cc2

+ 11
- 0
README.md Переглянути файл

@@ -18,6 +18,11 @@ Add the following line to the list ```INSTALLED_APPS```:
18 18
 ```
19 19
     'users.apps.UsersConfig',
20 20
 ``` 
21
+and this line to let django know the login url
22
+```
23
+LOGIN_URL = 'users-login'
24
+```
25
+
21 26
 
22 27
 ### Configurations in your urls.py
23 28
 and add the following line to the list ```urlpatterns```:
@@ -31,6 +36,12 @@ All parameters can be added in the django ```settings.py``` or in a ```config.py
31 36
 #### USERS_SELF_REGISTRATION
32 37
 This parameter can be ```True``` or ```False```. It enables or disables the self registration.
33 38
 
39
+#### USERS_MAIL_VALIDATION
40
+This parameter can be ```True``` or ```False```. It enables or disables the mail validation after self registration.
41
+
42
+#### USERS_ADMIN_ACTIVATION
43
+This parameter can be ```True``` or ```False```. It enables or disables the activation by an admin after mail validation.
44
+
34 45
 
35 46
 ## Usage
36 47
 ### Actionabr

+ 3
- 1
context.py Переглянути файл

@@ -11,11 +11,13 @@ PROFILE_ENTRY_UID = 'profile-main'
11 11
 REGISTER_ENTRY_UID = 'register-main'
12 12
 
13 13
 
14
-def context_adaption(context, request, title):
14
+def context_adaption(context, request, title, **kwargs):
15 15
     context.set_additional_title(title)
16 16
     menubar(context[context.MENUBAR], request)
17 17
     context[context.NAVIGATIONBAR].append_entry(*empty_entry_parameters(request))
18 18
     actionbar(context[context.ACTIONBAR], request)
19
+    for key in kwargs:
20
+        context[key] = kwargs[key]
19 21
 
20 22
 
21 23
 def menubar(bar, request):

+ 58
- 0
emails.py Переглянути файл

@@ -0,0 +1,58 @@
1
+from django.conf import settings
2
+from django.contrib.auth.models import User
3
+from django.contrib.sites.shortcuts import get_current_site
4
+from django.core.mail import EmailMessage, send_mail, mail_admins
5
+from django.template.loader import render_to_string
6
+from django.utils.encoding import force_bytes
7
+from django.utils.http import urlsafe_base64_encode
8
+from . tokens import generate_token
9
+from users import parameter
10
+
11
+
12
+def send_activation_mail(new_user, request):
13
+    email_subject = "Activate Account"
14
+    message2 = render_to_string('users/email_activation.html', {
15
+        'name': new_user.first_name,
16
+        'base_url': request.build_absolute_uri('/')[:-1],
17
+        'pk': new_user.pk,
18
+    })
19
+    email = EmailMessage(
20
+        email_subject,
21
+        message2,
22
+        settings.EMAIL_HOST_USER,
23
+    )
24
+    mail_admins(email_subject, message2, fail_silently=True)
25
+
26
+
27
+def send_validation_failed(uid, token):
28
+    subject = "E-Mail validation failed!"
29
+    message = f"uid = {uid}\ntoken = '{token}'"
30
+    mail_admins(subject, message, fail_silently=True)
31
+
32
+
33
+def send_welcome_mail(new_user: User):
34
+    subject = "Welcome!"
35
+    message = """Hello %(username)s.
36
+
37
+Welcome!
38
+
39
+Thank you for registration.""" % {"username": new_user.first_name}
40
+    message += "\n\n" + parameter.registration_flow_description(new_user.username)
41
+    send_mail(subject, message, settings.EMAIL_FROM, [new_user.email], fail_silently=True)
42
+
43
+
44
+def send_validation_mail(new_user, request):
45
+    email_subject = "Confirm your Email"
46
+    message2 = render_to_string('users/email_confirmation.html', {
47
+        'name': new_user.first_name,
48
+        'base_url': request.build_absolute_uri('/')[:-1],
49
+        'uid': urlsafe_base64_encode(force_bytes(new_user.pk)),
50
+        'token': generate_token.make_token(new_user)
51
+    })
52
+    email = EmailMessage(
53
+        email_subject,
54
+        message2,
55
+        settings.EMAIL_HOST_USER,
56
+        [new_user.email],
57
+    )
58
+    send_mail(email_subject, message2, settings.EMAIL_FROM, [new_user.email], fail_silently=True)

+ 10
- 2
forms.py Переглянути файл

@@ -1,6 +1,6 @@
1 1
 from django import forms
2 2
 from django.contrib.auth.models import User
3
-from django.contrib.auth.forms import UserCreationForm
3
+from django.contrib.auth.forms import UserCreationForm, UserChangeForm
4 4
 from .models import UserProfile
5 5
 
6 6
 
@@ -9,7 +9,7 @@ class UserRegistrationForm(UserCreationForm):
9 9
 
10 10
     class Meta:
11 11
         model = User
12
-        fields = ['username', 'email', 'password1', 'password2']
12
+        fields = ['username', 'email', 'first_name', 'last_name', 'password1', 'password2']
13 13
 
14 14
 
15 15
 class UserProfileForm(forms.ModelForm):
@@ -22,3 +22,11 @@ class UserProfileFormLanguageOnly(forms.ModelForm):
22 22
     class Meta:
23 23
         model = UserProfile
24 24
         fields = ['language_code']
25
+
26
+
27
+class UserActivationForm(UserChangeForm):
28
+    password = None
29
+
30
+    class Meta:
31
+        model = User
32
+        fields = ['is_active', 'is_staff']

+ 18
- 0
migrations/0003_userprofile_mail_validated.py Переглянути файл

@@ -0,0 +1,18 @@
1
+# Generated by Django 5.1.2 on 2024-10-26 12:49
2
+
3
+from django.db import migrations, models
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ('users', '0002_alter_userprofile_id_alter_userprofile_language_code_and_more'),
10
+    ]
11
+
12
+    operations = [
13
+        migrations.AddField(
14
+            model_name='userprofile',
15
+            name='mail_validated',
16
+            field=models.BooleanField(default=False),
17
+        ),
18
+    ]

+ 1
- 0
models.py Переглянути файл

@@ -29,6 +29,7 @@ class UserProfile(models.Model):
29 29
     user = models.OneToOneField(User, unique=True, on_delete=models.CASCADE)
30 30
     timezone = models.CharField(max_length=150, default='UTC', choices=[(t, t) for t in pytz.common_timezones])
31 31
     language_code = models.CharField(max_length=150, default='en', choices=settings.LANGUAGES)
32
+    mail_validated = models.BooleanField(default=False)
32 33
 
33 34
     def export_key(self):
34 35
         return self.user.username

+ 16
- 4
parameter.py Переглянути файл

@@ -1,15 +1,16 @@
1 1
 import config
2 2
 from django.conf import settings
3
+from django.utils.translation import gettext as _
3 4
 
4 5
 USERS_SELF_REGISTRATION = "USERS_SELF_REGISTRATION"
5
-# USERS_MAIL_VALIDATION = "USERS_MAIL_VALIDATION"
6
-# USERS_MAIL_INFORMATION = "USERS_MAIL_INFORMATION"
6
+USERS_MAIL_VALIDATION = "USERS_MAIL_VALIDATION"
7
+USERS_ADMIN_ACTIVATION = "USERS_ADMIN_ACTIVATION"
7 8
 
8 9
 
9 10
 DEFAULTS = {
10 11
     USERS_SELF_REGISTRATION: False,
11
-    # USERS_MAIL_VALIDATION: True,
12
-    # USERS_MAIL_INFORMATION: True,
12
+    USERS_MAIL_VALIDATION: True,
13
+    USERS_ADMIN_ACTIVATION: True,
13 14
 }
14 15
 
15 16
 
@@ -24,3 +25,14 @@ def get(key):
24 25
             data = DEFAULTS.get(key)
25 26
 
26 27
     return data
28
+
29
+
30
+def registration_flow_description(username):
31
+    if not get(USERS_MAIL_VALIDATION) and not get(USERS_ADMIN_ACTIVATION):
32
+        return _("Your account has been created. You are now able to Login as %s.") % username
33
+    elif get(USERS_MAIL_VALIDATION) and get(USERS_ADMIN_ACTIVATION):
34
+        return _("Your account has been created. You'll get an email to validate your account. Then you have to wait for the activation by an administrator.")
35
+    elif get(USERS_MAIL_VALIDATION):
36
+        return _("Your account has been created. You'll get an email to validate your account. After validation you are able to Login as %s.") % username
37
+    else:
38
+        return _("Your account has been created. You have to wait for the activation by an administrator.")

+ 12
- 0
templates/users/activate.html Переглянути файл

@@ -0,0 +1,12 @@
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" name="submit" value="{% trans "Submit" %}" class="button" />
10
+      <input type="submit" name="delete" value="{% trans "Delete user" %}" class="button" />
11
+    </form>
12
+{% endblock content %}

+ 8
- 0
templates/users/email_activation.html Переглянути файл

@@ -0,0 +1,8 @@
1
+{% autoescape off %}
2
+
3
+ Hello admins.
4
+ 
5
+ The User {{ username }} validated the email address. Please follow the link below to activate or delete the account.
6
+ Activation Link: {{ base_url }}{% url 'users-activate' pk=pk %} 
7
+
8
+ {% endautoescape %}

+ 8
- 0
templates/users/email_confirmation.html Переглянути файл

@@ -0,0 +1,8 @@
1
+{% autoescape off %}
2
+
3
+ Hello {{ name }}.
4
+ 
5
+ Please verify your email by clicking on the following link.
6
+ Confirmation Link: {{ base_url }}{% url 'users-validate' uidb64=uid token=token %} 
7
+
8
+ {% endautoescape %}

+ 9
- 0
tokens.py Переглянути файл

@@ -0,0 +1,9 @@
1
+from django.contrib.auth.tokens import PasswordResetTokenGenerator
2
+
3
+
4
+class AppTokenGenerator(PasswordResetTokenGenerator):
5
+    def _make_hash_value(self, user, timestamp):
6
+        return f"{user.is_active}{user.pk}{timestamp}"
7
+
8
+
9
+generate_token = AppTokenGenerator()

+ 2
- 0
urls.py Переглянути файл

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

+ 101
- 8
views.py Переглянути файл

@@ -7,12 +7,17 @@ from django.contrib.auth import login as django_login
7 7
 from django.contrib.auth import logout as django_logout
8 8
 from django.contrib.auth.decorators import login_required
9 9
 from django.contrib.auth.forms import AuthenticationForm
10
+from django.contrib.auth.models import User
11
+from django.utils.encoding import force_str
12
+from django.utils.http import urlsafe_base64_decode
10 13
 from django.utils.translation import gettext as _
11
-from .forms import UserRegistrationForm, UserProfileForm
14
+from .forms import UserRegistrationForm, UserProfileForm, UserActivationForm
12 15
 import logging
13 16
 from .models import get_userprofile
14 17
 from themes import Context
18
+from . tokens import generate_token
15 19
 import users
20
+from users import emails
16 21
 from users import parameter
17 22
 
18 23
 logger = logging.getLogger(settings.ROOT_LOGGER_NAME).getChild(__name__)
@@ -66,9 +71,16 @@ def register(request):
66 71
         else:
67 72
             form = UserRegistrationForm(request.POST)
68 73
             if form.is_valid():
74
+                # Deactivate the user, if validation or activation is required
75
+                if parameter.get(parameter.USERS_MAIL_VALIDATION) or parameter.get(parameter.USERS_ADMIN_ACTIVATION):
76
+                    form.instance.is_active = False
69 77
                 form.save()
70
-                messages.success(request, _('Your account has been created! You are able to log in as %(username)s.') %
71
-                                 {'username': form.cleaned_data.get('username')})
78
+                # Send welcome message
79
+                emails.send_welcome_mail(form.instance)
80
+                if parameter.get(parameter.USERS_MAIL_VALIDATION):
81
+                    emails.send_validation_mail(form.instance, request)
82
+                # Add success message
83
+                messages.success(request, parameter.registration_flow_description(form.cleaned_data.get('username')))
72 84
                 return redirect('users-login')
73 85
             else:
74 86
                 messages.error(request, _('Registration failed!'))
@@ -95,12 +107,17 @@ def login(request):
95 107
             messages.success(request, _('You are now logged in as %(username)s.') % {'username': username})
96 108
             return redirect(request.GET.get('next') or '/')
97 109
         else:
98
-            if parameter.get(parameter.USERS_SELF_REGISTRATION):
99
-                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>.') %
100
-                               {'url_register': users.url_register(request), 'url_recover': users.url_password_recovery(request)})
110
+            username = form.cleaned_data.get('username')
111
+            user = User.objects.get(username=username)
112
+            if user.is_active:
113
+                if parameter.get(parameter.USERS_SELF_REGISTRATION):
114
+                    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>.') %
115
+                                   {'url_register': users.url_register(request), 'url_recover': users.url_password_recovery(request)})
116
+                else:
117
+                    messages.error(request, _('Login failed! You can do a password recorvery <a href="%(url_recover)s">here</a>.') %
118
+                                   {'url_recover': users.url_password_recovery(request)})
101 119
             else:
102
-                messages.error(request, _('Login failed! You can do a password recorvery <a href="%(url_recover)s">here</a>.') %
103
-                               {'url_recover': users.url_password_recovery(request)})
120
+                messages.info(request, _("The account is deactivated. Confirm your email adress and wait for the administrator to activate your account."))
104 121
 
105 122
     context['form'] = form
106 123
     return render(request, 'users/login.html', context)
@@ -120,3 +137,79 @@ def logout(request):
120 137
     for variable in session_cache:
121 138
         request.session[variable] = session_cache[variable]
122 139
     return redirect(request.GET.get('next') or '/')
140
+
141
+
142
+def validate(request, uidb64, token):
143
+    context = Context(request)      # needs to be executed first because of time mesurement
144
+    try:
145
+        uid = force_str(urlsafe_base64_decode(uidb64))
146
+    except (TypeError, ValueError, OverflowError, User.DoesNotExist):
147
+        uid = None
148
+        myuser = None
149
+    else:
150
+        try:
151
+            myuser = User.objects.get(pk=uid)
152
+        except User.DoesNotExist:
153
+            myuser = None
154
+
155
+    if myuser is not None and generate_token.check_token(myuser, token):
156
+        # Store mail validation to user profile
157
+        profile = get_userprofile(myuser)
158
+        profile.mail_validated = True
159
+        profile.save()
160
+        if not parameter.get(parameter.USERS_ADMIN_ACTIVATION):
161
+            # Activate user
162
+            myuser.is_active = True
163
+            myuser.save()
164
+            messages.success(request, _("Your Account has been activated."))
165
+            return redirect('users-login')
166
+        else:
167
+            emails.send_activation_mail(myuser, request)
168
+            messages.success(request, _("Your Email has been validated. Wait for the administrator to activate your account"))
169
+            return redirect("/")
170
+    else:
171
+        context_adaption(
172
+            context,
173
+            request,
174
+            _('Validation failed'),
175
+        )
176
+        messages.info(request, _("Vaildation failed. The system administrator will be informed."))
177
+        emails.send_validation_failed(uid, token)
178
+        return redirect("/")
179
+
180
+
181
+@login_required
182
+def activate(request, pk):
183
+    context = Context(request)      # needs to be executed first because of time mesurement
184
+    if not request.POST:
185
+        if request.user.is_superuser:
186
+            user_to_be_activated = User.objects.get(pk=pk)
187
+            if not user_to_be_activated.is_active:
188
+                user_to_be_activated.is_active = True
189
+                form = UserActivationForm(instance=user_to_be_activated)
190
+                context_adaption(
191
+                    context,
192
+                    request,
193
+                    _('Activation of user: %s') % f"{user_to_be_activated.username} - {user_to_be_activated.email}",
194
+                    form=form,
195
+                )
196
+                return render(request, 'users/activate.html', context)
197
+            else:
198
+                messages.error(request, _("The user %s is already active.") % user_to_be_activated.username)
199
+        else:
200
+            messages.error(request, _("You are no administrator. Log in as administrator and try again!"))
201
+    else:
202
+        submit = request.POST.get("submit")
203
+        delete = request.POST.get("delete")
204
+        user_to_be_activated = User.objects.get(pk=pk)
205
+        if submit:
206
+            form = UserActivationForm(request.POST, instance=user_to_be_activated)
207
+            if form.is_valid():
208
+                form.save()
209
+                messages.info(request, _("User permissions changed."))
210
+            else:
211
+                messages.error(request, _("Error while processing user change form"))
212
+        if delete:
213
+            user_to_be_activated.delete()
214
+            messages.info(request, _("User deleted."))
215
+    return redirect("/")

Завантаження…
Відмінити
Зберегти