From 64626335d2b144846f0350c07b71fc5bd36cd697 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Thu, 16 Apr 2020 22:06:14 +0200 Subject: [PATCH] Create EMAIL_NOT_YET_CONFIRMED state --- freeradius_utils/auth.py | 2 +- logs/views.py | 10 +++++ re2o/utils.py | 2 +- users/forms.py | 14 +++++++ users/management/commands/archive.py | 1 + users/models.py | 40 ++++++++++++++++++- .../users/email_confirmation_request | 33 +++++++++++++++ users/views.py | 35 ++++++++++++++++ 8 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 users/templates/users/email_confirmation_request diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 496f2f3f..daebf95d 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -469,7 +469,7 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, mac_address): RadiusOption.get_attributes("non_member_attributes", attributes_kwargs), ) for user in room_user: - if user.is_ban() or user.state != User.STATE_ACTIVE: + if user.is_ban() or user.state not in [User.STATE_ACTIVE, User.STATE_EMAIL_NOT_YET_CONFIRMED]: return ( sw_name, room, diff --git a/logs/views.py b/logs/views.py index 7c509134..99ecf37b 100644 --- a/logs/views.py +++ b/logs/views.py @@ -260,6 +260,16 @@ def stats_general(request): ), Club.objects.filter(state=Club.STATE_NOT_YET_ACTIVE).count(), ], + "email_not_confirmed_users": [ + _("Email not yet confirmed users"), + User.objects.filter(state=User.STATE_EMAIL_NOT_YET_CONFIRMED).count(), + ( + Adherent.objects.filter( + state=Adherent.STATE_EMAIL_NOT_YET_CONFIRMED + ).count() + ), + Club.objects.filter(state=Club.STATE_EMAIL_NOT_YET_CONFIRMED).count(), + ], "adherent_users": [ _("Contributing members"), _all_adherent.count(), diff --git a/re2o/utils.py b/re2o/utils.py index f4abc57c..61c45c0c 100644 --- a/re2o/utils.py +++ b/re2o/utils.py @@ -116,7 +116,7 @@ def all_has_access(search_time=None, including_asso=True): if search_time is None: search_time = timezone.now() filter_user = ( - Q(state=User.STATE_ACTIVE) + (Q(state=User.STATE_ACTIVE) | Q(state=User.STATE_EMAIL_NOT_YET_CONFIRMED)) & ~Q( ban__in=Ban.objects.filter( Q(date_start__lt=search_time) & Q(date_end__gt=search_time) diff --git a/users/forms.py b/users/forms.py index 3cac1d16..bc88a1f4 100644 --- a/users/forms.py +++ b/users/forms.py @@ -117,6 +117,20 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm): user.save() +class ConfirmMailForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm): + """Formulaire de confirmation de l'email de l'utilisateur""" + class Meta: + model = User + fields = [] + + def save(self, commit=True): + """Confirmation de l'email""" + user = super(ConfirmMailForm, self).save(commit=False) + user.confirm_mail() + user.set_active() + user.save() + + class UserCreationForm(FormRevMixin, forms.ModelForm): """A form for creating new users. Includes all the required fields, plus a repeated password. diff --git a/users/management/commands/archive.py b/users/management/commands/archive.py index 1e4601a0..d730cfcd 100644 --- a/users/management/commands/archive.py +++ b/users/management/commands/archive.py @@ -77,6 +77,7 @@ class Command(BaseCommand): .exclude(id__in=all_has_access(search_time=date)) .exclude(state=User.STATE_NOT_YET_ACTIVE) .exclude(state=User.STATE_FULL_ARCHIVE) + .exclude(state=User.STATE_EMAIL_NOT_YET_CONFIRMED) ) if show: diff --git a/users/models.py b/users/models.py index 75a38aa9..f93048c2 100755 --- a/users/models.py +++ b/users/models.py @@ -176,12 +176,14 @@ class User( STATE_ARCHIVE = 2 STATE_NOT_YET_ACTIVE = 3 STATE_FULL_ARCHIVE = 4 + STATE_EMAIL_NOT_YET_CONFIRMED = 5 STATES = ( (0, _("Active")), (1, _("Disabled")), (2, _("Archived")), (3, _("Not yet active")), (4, _("Fully archived")), + (5, _("Waiting for email confirmation")), ) surname = models.CharField(max_length=255) @@ -326,6 +328,7 @@ class User( return ( self.state == self.STATE_ACTIVE or self.state == self.STATE_NOT_YET_ACTIVE + or self.state == self.STATE_EMAIL_NOT_YET_CONFIRMED or ( allow_archived and self.state in (self.STATE_ARCHIVE, self.STATE_FULL_ARCHIVE) @@ -480,7 +483,7 @@ class User( def has_access(self): """ Renvoie si un utilisateur a accès à internet """ return ( - self.state == User.STATE_ACTIVE + self.state in [User.STATE_ACTIVE, User.STATE_EMAIL_NOT_YET_CONFIRMED] and not self.is_ban() and (self.is_connected() or self.is_whitelisted()) ) or self == AssoOption.get_cached_value("utilisateur_asso") @@ -665,6 +668,7 @@ class User( Si l'instance n'existe pas, on crée le ldapuser correspondant""" if sys.version_info[0] >= 3 and ( self.state == self.STATE_ACTIVE + or self.state == STATE_EMAIL_NOT_YET_CONFIRMED or self.state == self.STATE_ARCHIVE or self.state == self.STATE_DISABLED ): @@ -783,6 +787,34 @@ class User( ) return + def confirm_email_address_mail(self, request): + """Prend en argument un request, envoie un mail pour + confirmer l'adresse""" + req = Request() + req.type = Request.EMAIL + req.user = self + req.save() + template = loader.get_template("users/email_confirmation_request") + context = { + "name": req.user.get_full_name(), + "asso": AssoOption.get_cached_value("name"), + "asso_mail": AssoOption.get_cached_value("contact"), + "site_name": GeneralOption.get_cached_value("site_name"), + "url": request.build_absolute_uri( + reverse("users:process", kwargs={"token": req.token}) + ), + "expire_in": str(GeneralOption.get_cached_value("req_expire_hrs")), + } + send_mail( + "Confirmation de l'email de %(name)s / Email confirmation for " + "%(name)s" % {"name": AssoOption.get_cached_value("name")}, + template.render(context), + GeneralOption.get_cached_value("email_from"), + [req.user.email], + fail_silently=False, + ) + return + def autoregister_machine(self, mac_address, nas_type): """ Fonction appellée par freeradius. Enregistre la mac pour une machine inconnue sur le compte de l'user""" @@ -845,6 +877,12 @@ class User( self.pwd_ntlm = hashNT(password) return + def confirm_mail(self): + """Marque l'email de l'utilisateur comme confirmé""" + # Let the "set_active" method handle + self.state = self.STATE_NOT_YET_ACTIVE + self.set_active() + @cached_property def email_address(self): if ( diff --git a/users/templates/users/email_confirmation_request b/users/templates/users/email_confirmation_request new file mode 100644 index 00000000..b3385e02 --- /dev/null +++ b/users/templates/users/email_confirmation_request @@ -0,0 +1,33 @@ +Bonjour {{ name }}, + +Vous trouverez ci-dessous une URL permettant de confirmer votre +adresse mail pour votre compte {{ site_name }}. Celui-ci vous permet de gérer l'ensemble +de vos équipements, votre compte, vos factures, et tous les services proposés sur le réseau. + + {{ url }} + +Contactez les administrateurs si vous n'êtes pas à l'origine de cette requête. + +Ce lien expirera dans {{ expire_in }} heures. + +Respectueusement, + +L'équipe de {{ asso }} (contact : {{ asso_mail }}). + +--- + +Hello {{ name }}, + +You will find below an URL allowing you to confirm the email address of your account +on {{ site_name }}. It enables you to manage your devices, your account, your invoices, and all +the services offered on the network. + + {{ url }} + +Contact the administrators if you didn't request this. + +This link will expire in {{ expire_in }} hours. + +Regards, + +The {{ asso }} team (contact: {{ asso_mail }}). diff --git a/users/views.py b/users/views.py index 3f41a990..4f4a62d3 100644 --- a/users/views.py +++ b/users/views.py @@ -105,6 +105,7 @@ from .forms import ( ClubForm, MassArchiveForm, PassForm, + ConfirmMailForm, ResetPasswordForm, ClubAdminandMembersForm, GroupForm, @@ -126,6 +127,7 @@ def new_user(request): # Use "is False" so that if None, the email is sent if is_set_password_allowed and user.should_send_password_reset_email is False: + user.confirm_email_address_mail(request) messages.success( request, _("The user %s was created.") @@ -737,6 +739,7 @@ def mass_archive(request): .exclude(id__in=all_has_access(search_time=date)) .exclude(state=User.STATE_NOT_YET_ACTIVE) .exclude(state=User.STATE_FULL_ARCHIVE) + .exclude(state=User.STATE_EMAIL_NOT_YET_CONFIRMED) ) if not full_archive: to_archive_list = to_archive_list.exclude(state=User.STATE_ARCHIVE) @@ -1020,6 +1023,38 @@ def process_passwd(request, req): ) +def confirm_email(request, token): + """Lien pour la confirmation de l'email""" + valid_reqs = Request.objects.filter(expires_at__gt=timezone.now()) + req = get_object_or_404(valid_reqs, token=token) + + if req.type == Request.EMAIL: + return process_email(request, req) + else: + messages.error(request, _("Error: please contact an admin.")) + redirect(reverse("index")) + + +def process_email(request, req): + """Process la confirmation de mail, renvoie le formulaire + de validation""" + user = req.user + u_form = ConfirmMailForm(request.POST or None, instance=user, user=request.user) + if u_form.is_valid(): + with transaction.atomic(), reversion.create_revision(): + u_form.save() + reversion.set_comment("Email confirmation") + req.delete() + messages.success(request, _("The email was confirmed.")) + return redirect(reverse("index")) + + return form( + {"userform": u_form, "action_name": _("Confirm the email")}, + "users/user.html", + request, + ) + + @login_required def initial_register(request): switch_ip = request.GET.get("switch_ip", None)