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',
```
and this line to let django know the login url
```
LOGIN_URL = 'users-login'
```
### Configurations in your urls.py
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
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
### Actionabr

View File

@ -11,11 +11,13 @@ PROFILE_ENTRY_UID = 'profile-main'
REGISTER_ENTRY_UID = 'register-main'
def context_adaption(context, request, title):
def context_adaption(context, request, title, **kwargs):
context.set_additional_title(title)
menubar(context[context.MENUBAR], request)
context[context.NAVIGATIONBAR].append_entry(*empty_entry_parameters(request))
actionbar(context[context.ACTIONBAR], request)
for key in kwargs:
context[key] = kwargs[key]
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.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import UserProfile
@ -9,7 +9,7 @@ class UserRegistrationForm(UserCreationForm):
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
fields = ['username', 'email', 'first_name', 'last_name', 'password1', 'password2']
class UserProfileForm(forms.ModelForm):
@ -22,3 +22,11 @@ class UserProfileFormLanguageOnly(forms.ModelForm):
class Meta:
model = UserProfile
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)
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)
mail_validated = models.BooleanField(default=False)
def export_key(self):
return self.user.username

View File

@ -1,15 +1,16 @@
import config
from django.conf import settings
from django.utils.translation import gettext as _
USERS_SELF_REGISTRATION = "USERS_SELF_REGISTRATION"
# USERS_MAIL_VALIDATION = "USERS_MAIL_VALIDATION"
# USERS_MAIL_INFORMATION = "USERS_MAIL_INFORMATION"
USERS_MAIL_VALIDATION = "USERS_MAIL_VALIDATION"
USERS_ADMIN_ACTIVATION = "USERS_ADMIN_ACTIVATION"
DEFAULTS = {
USERS_SELF_REGISTRATION: False,
# USERS_MAIL_VALIDATION: True,
# USERS_MAIL_INFORMATION: True,
USERS_MAIL_VALIDATION: True,
USERS_ADMIN_ACTIVATION: True,
}
@ -24,3 +25,14 @@ def get(key):
data = DEFAULTS.get(key)
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('profile', views.profile, name='users-profile'),
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.decorators import login_required
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 .forms import UserRegistrationForm, UserProfileForm
from .forms import UserRegistrationForm, UserProfileForm, UserActivationForm
import logging
from .models import get_userprofile
from themes import Context
from . tokens import generate_token
import users
from users import emails
from users import parameter
logger = logging.getLogger(settings.ROOT_LOGGER_NAME).getChild(__name__)
@ -66,9 +71,16 @@ def register(request):
else:
form = UserRegistrationForm(request.POST)
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()
messages.success(request, _('Your account has been created! You are able to log in as %(username)s.') %
{'username': form.cleaned_data.get('username')})
# Send welcome message
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')
else:
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})
return redirect(request.GET.get('next') or '/')
else:
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)})
username = form.cleaned_data.get('username')
user = User.objects.get(username=username)
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:
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)})
messages.info(request, _("The account is deactivated. Confirm your email adress and wait for the administrator to activate your account."))
context['form'] = form
return render(request, 'users/login.html', context)
@ -120,3 +137,79 @@ def logout(request):
for variable in session_cache:
request.session[variable] = session_cache[variable]
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("/")