Self registration with mail validation and admin activation implemented

This commit is contained in:
Dirk Alders 2024-10-26 20:04:32 +02:00
parent c9532aaf37
commit d18f439cc2
13 changed files with 257 additions and 15 deletions

View File

@ -18,6 +18,11 @@ Add the following line to the list ```INSTALLED_APPS```:
``` ```
'users.apps.UsersConfig', 'users.apps.UsersConfig',
``` ```
and this line to let django know the login url
```
LOGIN_URL = 'users-login'
```
### Configurations in your urls.py ### Configurations in your urls.py
and add the following line to the list ```urlpatterns```: 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
#### USERS_SELF_REGISTRATION #### USERS_SELF_REGISTRATION
This parameter can be ```True``` or ```False```. It enables or disables the self registration. This parameter can be ```True``` or ```False```. It enables or disables the self registration.
#### USERS_MAIL_VALIDATION
This parameter can be ```True``` or ```False```. It enables or disables the mail validation after self registration.
#### USERS_ADMIN_ACTIVATION
This parameter can be ```True``` or ```False```. It enables or disables the activation by an admin after mail validation.
## Usage ## Usage
### Actionabr ### Actionabr

View File

@ -11,11 +11,13 @@ PROFILE_ENTRY_UID = 'profile-main'
REGISTER_ENTRY_UID = 'register-main' REGISTER_ENTRY_UID = 'register-main'
def context_adaption(context, request, title): def context_adaption(context, request, title, **kwargs):
context.set_additional_title(title) context.set_additional_title(title)
menubar(context[context.MENUBAR], request) menubar(context[context.MENUBAR], request)
context[context.NAVIGATIONBAR].append_entry(*empty_entry_parameters(request)) context[context.NAVIGATIONBAR].append_entry(*empty_entry_parameters(request))
actionbar(context[context.ACTIONBAR], request) actionbar(context[context.ACTIONBAR], request)
for key in kwargs:
context[key] = kwargs[key]
def menubar(bar, request): def menubar(bar, request):

58
emails.py Normal file
View File

@ -0,0 +1,58 @@
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMessage, send_mail, mail_admins
from django.template.loader import render_to_string
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from . tokens import generate_token
from users import parameter
def send_activation_mail(new_user, request):
email_subject = "Activate Account"
message2 = render_to_string('users/email_activation.html', {
'name': new_user.first_name,
'base_url': request.build_absolute_uri('/')[:-1],
'pk': new_user.pk,
})
email = EmailMessage(
email_subject,
message2,
settings.EMAIL_HOST_USER,
)
mail_admins(email_subject, message2, fail_silently=True)
def send_validation_failed(uid, token):
subject = "E-Mail validation failed!"
message = f"uid = {uid}\ntoken = '{token}'"
mail_admins(subject, message, fail_silently=True)
def send_welcome_mail(new_user: User):
subject = "Welcome!"
message = """Hello %(username)s.
Welcome!
Thank you for registration.""" % {"username": new_user.first_name}
message += "\n\n" + parameter.registration_flow_description(new_user.username)
send_mail(subject, message, settings.EMAIL_FROM, [new_user.email], fail_silently=True)
def send_validation_mail(new_user, request):
email_subject = "Confirm your Email"
message2 = render_to_string('users/email_confirmation.html', {
'name': new_user.first_name,
'base_url': request.build_absolute_uri('/')[:-1],
'uid': urlsafe_base64_encode(force_bytes(new_user.pk)),
'token': generate_token.make_token(new_user)
})
email = EmailMessage(
email_subject,
message2,
settings.EMAIL_HOST_USER,
[new_user.email],
)
send_mail(email_subject, message2, settings.EMAIL_FROM, [new_user.email], fail_silently=True)

View File

@ -1,6 +1,6 @@
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 from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import UserProfile from .models import UserProfile
@ -9,7 +9,7 @@ class UserRegistrationForm(UserCreationForm):
class Meta: class Meta:
model = User model = User
fields = ['username', 'email', 'password1', 'password2'] fields = ['username', 'email', 'first_name', 'last_name', 'password1', 'password2']
class UserProfileForm(forms.ModelForm): class UserProfileForm(forms.ModelForm):
@ -22,3 +22,11 @@ class UserProfileFormLanguageOnly(forms.ModelForm):
class Meta: class Meta:
model = UserProfile model = UserProfile
fields = ['language_code'] fields = ['language_code']
class UserActivationForm(UserChangeForm):
password = None
class Meta:
model = User
fields = ['is_active', 'is_staff']

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-10-26 12:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0002_alter_userprofile_id_alter_userprofile_language_code_and_more'),
]
operations = [
migrations.AddField(
model_name='userprofile',
name='mail_validated',
field=models.BooleanField(default=False),
),
]

View File

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

View File

@ -1,15 +1,16 @@
import config import config
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext as _
USERS_SELF_REGISTRATION = "USERS_SELF_REGISTRATION" USERS_SELF_REGISTRATION = "USERS_SELF_REGISTRATION"
# USERS_MAIL_VALIDATION = "USERS_MAIL_VALIDATION" USERS_MAIL_VALIDATION = "USERS_MAIL_VALIDATION"
# USERS_MAIL_INFORMATION = "USERS_MAIL_INFORMATION" USERS_ADMIN_ACTIVATION = "USERS_ADMIN_ACTIVATION"
DEFAULTS = { DEFAULTS = {
USERS_SELF_REGISTRATION: False, USERS_SELF_REGISTRATION: False,
# USERS_MAIL_VALIDATION: True, USERS_MAIL_VALIDATION: True,
# USERS_MAIL_INFORMATION: True, USERS_ADMIN_ACTIVATION: True,
} }
@ -24,3 +25,14 @@ def get(key):
data = DEFAULTS.get(key) data = DEFAULTS.get(key)
return data return data
def registration_flow_description(username):
if not get(USERS_MAIL_VALIDATION) and not get(USERS_ADMIN_ACTIVATION):
return _("Your account has been created. You are now able to Login as %s.") % username
elif get(USERS_MAIL_VALIDATION) and get(USERS_ADMIN_ACTIVATION):
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.")
elif get(USERS_MAIL_VALIDATION):
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
else:
return _("Your account has been created. You have to wait for the activation by an administrator.")

View File

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

View File

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

View File

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

9
tokens.py Normal file
View File

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

View File

@ -7,4 +7,6 @@ urlpatterns = [
path('password_recovery', views.password_recovery, name='users-password-recovery'), 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('validate/<uidb64>/<token>', views.validate, name='users-validate'),
path('activate/<pk>', views.activate, name='users-activate'),
] ]

109
views.py
View File

@ -7,12 +7,17 @@ from django.contrib.auth import login as django_login
from django.contrib.auth import logout as django_logout from django.contrib.auth import logout as django_logout
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User
from django.utils.encoding import force_str
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 from .forms import UserRegistrationForm, UserProfileForm, UserActivationForm
import logging import logging
from .models import get_userprofile from .models import get_userprofile
from themes import Context from themes import Context
from . tokens import generate_token
import users import users
from users import emails
from users import parameter from users import parameter
logger = logging.getLogger(settings.ROOT_LOGGER_NAME).getChild(__name__) logger = logging.getLogger(settings.ROOT_LOGGER_NAME).getChild(__name__)
@ -66,9 +71,16 @@ def register(request):
else: else:
form = UserRegistrationForm(request.POST) form = UserRegistrationForm(request.POST)
if form.is_valid(): if form.is_valid():
# Deactivate the user, if validation or activation is required
if parameter.get(parameter.USERS_MAIL_VALIDATION) or parameter.get(parameter.USERS_ADMIN_ACTIVATION):
form.instance.is_active = False
form.save() form.save()
messages.success(request, _('Your account has been created! You are able to log in as %(username)s.') % # Send welcome message
{'username': form.cleaned_data.get('username')}) emails.send_welcome_mail(form.instance)
if parameter.get(parameter.USERS_MAIL_VALIDATION):
emails.send_validation_mail(form.instance, request)
# Add success message
messages.success(request, parameter.registration_flow_description(form.cleaned_data.get('username')))
return redirect('users-login') return redirect('users-login')
else: else:
messages.error(request, _('Registration failed!')) messages.error(request, _('Registration failed!'))
@ -95,12 +107,17 @@ def login(request):
messages.success(request, _('You are now logged in as %(username)s.') % {'username': username}) messages.success(request, _('You are now logged in as %(username)s.') % {'username': username})
return redirect(request.GET.get('next') or '/') return redirect(request.GET.get('next') or '/')
else: else:
if parameter.get(parameter.USERS_SELF_REGISTRATION): username = form.cleaned_data.get('username')
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>.') % user = User.objects.get(username=username)
{'url_register': users.url_register(request), 'url_recover': users.url_password_recovery(request)}) if user.is_active:
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>.') %
{'url_register': users.url_register(request), 'url_recover': users.url_password_recovery(request)})
else:
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)})
else: else:
messages.error(request, _('Login failed! You can do a password recorvery <a href="%(url_recover)s">here</a>.') % messages.info(request, _("The account is deactivated. Confirm your email adress and wait for the administrator to activate your account."))
{'url_recover': users.url_password_recovery(request)})
context['form'] = form context['form'] = form
return render(request, 'users/login.html', context) return render(request, 'users/login.html', context)
@ -120,3 +137,79 @@ def logout(request):
for variable in session_cache: for variable in session_cache:
request.session[variable] = session_cache[variable] request.session[variable] = session_cache[variable]
return redirect(request.GET.get('next') or '/') return redirect(request.GET.get('next') or '/')
def validate(request, uidb64, token):
context = Context(request) # needs to be executed first because of time mesurement
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):
# Store mail validation to user profile
profile = get_userprofile(myuser)
profile.mail_validated = True
profile.save()
if not parameter.get(parameter.USERS_ADMIN_ACTIVATION):
# Activate user
myuser.is_active = True
myuser.save()
messages.success(request, _("Your Account has been activated."))
return redirect('users-login')
else:
emails.send_activation_mail(myuser, request)
messages.success(request, _("Your Email has been validated. Wait for the administrator to activate your account"))
return redirect("/")
else:
context_adaption(
context,
request,
_('Validation failed'),
)
messages.info(request, _("Vaildation failed. The system administrator will be informed."))
emails.send_validation_failed(uid, token)
return redirect("/")
@login_required
def activate(request, pk):
context = Context(request) # needs to be executed first because of time mesurement
if not request.POST:
if request.user.is_superuser:
user_to_be_activated = User.objects.get(pk=pk)
if not user_to_be_activated.is_active:
user_to_be_activated.is_active = True
form = UserActivationForm(instance=user_to_be_activated)
context_adaption(
context,
request,
_('Activation of user: %s') % f"{user_to_be_activated.username} - {user_to_be_activated.email}",
form=form,
)
return render(request, 'users/activate.html', context)
else:
messages.error(request, _("The user %s is already active.") % user_to_be_activated.username)
else:
messages.error(request, _("You are no administrator. Log in as administrator and try again!"))
else:
submit = request.POST.get("submit")
delete = request.POST.get("delete")
user_to_be_activated = User.objects.get(pk=pk)
if submit:
form = UserActivationForm(request.POST, instance=user_to_be_activated)
if form.is_valid():
form.save()
messages.info(request, _("User permissions changed."))
else:
messages.error(request, _("Error while processing user change form"))
if delete:
user_to_be_activated.delete()
messages.info(request, _("User deleted."))
return redirect("/")