diff --git a/README.md b/README.md index c8273f2..a2e47d0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/context.py b/context.py index ceda0ca..2f7843e 100644 --- a/context.py +++ b/context.py @@ -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): diff --git a/emails.py b/emails.py new file mode 100644 index 0000000..01e20ce --- /dev/null +++ b/emails.py @@ -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) diff --git a/forms.py b/forms.py index e465bf7..8c45568 100644 --- a/forms.py +++ b/forms.py @@ -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'] diff --git a/migrations/0003_userprofile_mail_validated.py b/migrations/0003_userprofile_mail_validated.py new file mode 100644 index 0000000..8756d7a --- /dev/null +++ b/migrations/0003_userprofile_mail_validated.py @@ -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), + ), + ] diff --git a/models.py b/models.py index bf0fbf1..0ddcaf0 100644 --- a/models.py +++ b/models.py @@ -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 diff --git a/parameter.py b/parameter.py index a478afa..c3b33a6 100644 --- a/parameter.py +++ b/parameter.py @@ -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.") diff --git a/templates/users/activate.html b/templates/users/activate.html new file mode 100644 index 0000000..d124b10 --- /dev/null +++ b/templates/users/activate.html @@ -0,0 +1,12 @@ +{% extends "themes/"|add:settings.page_theme|add:"/base.html" %} +{% load i18n %} + +{% block content %} + +
+ {% csrf_token %} + {{ form.as_p }} + + +
+{% endblock content %} diff --git a/templates/users/email_activation.html b/templates/users/email_activation.html new file mode 100644 index 0000000..917580e --- /dev/null +++ b/templates/users/email_activation.html @@ -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 %} diff --git a/templates/users/email_confirmation.html b/templates/users/email_confirmation.html new file mode 100644 index 0000000..1d19a80 --- /dev/null +++ b/templates/users/email_confirmation.html @@ -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 %} diff --git a/tokens.py b/tokens.py new file mode 100644 index 0000000..9987d9d --- /dev/null +++ b/tokens.py @@ -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() diff --git a/urls.py b/urls.py index aa6537a..065bc68 100644 --- a/urls.py +++ b/urls.py @@ -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//', views.validate, name='users-validate'), + path('activate/', views.activate, name='users-activate'), ] diff --git a/views.py b/views.py index e24b3fb..2bc6589 100644 --- a/views.py +++ b/views.py @@ -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 here or you can register here.') % - {'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 here or you can register here.') % + {'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 here.') % + {'url_recover': users.url_password_recovery(request)}) else: - messages.error(request, _('Login failed! You can do a password recorvery here.') % - {'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("/")