From 5f0dfff4aedfeec2c722a34d970de3efb2d36e5c Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 24 Sep 2024 15:57:28 +0200 Subject: [PATCH] feat(accout): Add a view to self-create an account Various checks are in place to make sure that the user does not already have an account, and has accepted the rules of the association --- src/dgsi/forms.py | 8 ++ .../templates/dgsi/create_self_account.html | 15 +++ src/dgsi/templates/dgsi/legal_documents.html | 15 +++ src/dgsi/templates/dgsi/profile.html | 7 +- src/dgsi/urls.py | 9 +- src/dgsi/views.py | 98 +++++++++++++++++-- 6 files changed, 141 insertions(+), 11 deletions(-) create mode 100644 src/dgsi/templates/dgsi/create_self_account.html diff --git a/src/dgsi/forms.py b/src/dgsi/forms.py index 2bc31a7..404328c 100644 --- a/src/dgsi/forms.py +++ b/src/dgsi/forms.py @@ -33,3 +33,11 @@ class CreateKanidmAccountForm(forms.Form): help_text=_("Si selectionné, la personne sera ajoutée au groupe dgnum_members"), required=False, ) + + +class CreateSelfAccountForm(forms.Form): + displayname = CharField(label=_("Nom d'usage")) + mail = EmailField( + label=_("Adresse e-mail"), + help_text=_("De préférence l'adresse '@ens.psl.eu'"), + ) diff --git a/src/dgsi/templates/dgsi/create_self_account.html b/src/dgsi/templates/dgsi/create_self_account.html new file mode 100644 index 0000000..9e06865 --- /dev/null +++ b/src/dgsi/templates/dgsi/create_self_account.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} +

{% trans "Création d'un compte DGNum" %}

+
+ +
+ {% csrf_token %} + {% include "bulma/form.html" with form=form %} + + +
+{% endblock content %} diff --git a/src/dgsi/templates/dgsi/legal_documents.html b/src/dgsi/templates/dgsi/legal_documents.html index 1c65b99..36ec809 100644 --- a/src/dgsi/templates/dgsi/legal_documents.html +++ b/src/dgsi/templates/dgsi/legal_documents.html @@ -6,6 +6,21 @@

Documents Légaux


+ {% if user.kanidm is None %} + {% if show_message %} +
+ {% trans "Vous devez accepter les Statuts et le Règlement Intérieur de la DGNum avant de pouvoir créer un compte." %} +
+ {% else %} +
+ {% trans "Vous n'avez pas encore de compte DGNum, mais vous pouvez désormais en créer un." %} +
+ {% trans "Poursuivre la création d'un compte DGNum" %} +
+ {% endif %} + {% endif %} +
{% include "_legal_document.html" with document=statutes user_document=user.accepted_statutes title=_("Statuts") accept_question=_("Accepter les statuts") %} diff --git a/src/dgsi/templates/dgsi/profile.html b/src/dgsi/templates/dgsi/profile.html index eed8365..0790195 100644 --- a/src/dgsi/templates/dgsi/profile.html +++ b/src/dgsi/templates/dgsi/profile.html @@ -47,8 +47,11 @@ {% endfor %} {% else %} -
- {% trans "Pas de compte DGNum répertorié." %} +
+ {% trans "Pas de compte DGNum répertorié." %} +
+ {% trans "Créer un compte DGNum" %}
{% endif %} {% endblock content %} diff --git a/src/dgsi/urls.py b/src/dgsi/urls.py index ddc1e32..cf7945c 100644 --- a/src/dgsi/urls.py +++ b/src/dgsi/urls.py @@ -20,10 +20,15 @@ urlpatterns = [ ), # Account views path("accounts/profile/", views.ProfileView.as_view(), name="dgn-profile"), + path( + "accounts/create/", + views.CreateSelfAccountView.as_view(), + name="dgn-create_self_account", + ), path( "accounts/create-kanidm/", views.CreateKanidmAccountView.as_view(), - name="dgn-create_user", + name="dgn-create_kanidm_user", ), path( "accounts/forbidden/", @@ -33,7 +38,7 @@ urlpatterns = [ # Services views path("services/", views.ServiceListView.as_view(), name="dgn-services"), path( - "services/redirect/", + "services/redirect//", views.ServiceRedirectView.as_view(), name="dgn-services_redirect", ), diff --git a/src/dgsi/views.py b/src/dgsi/views.py index ba78f82..3d3df6c 100644 --- a/src/dgsi/views.py +++ b/src/dgsi/views.py @@ -2,10 +2,10 @@ from typing import Any, NamedTuple from asgiref.sync import async_to_sync from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin from django.contrib.messages.views import SuccessMessageMixin from django.core.mail import EmailMessage -from django.http import HttpRequest, HttpResponseBase +from django.http import HttpRequest, HttpResponseBase, HttpResponseRedirect from django.template.loader import render_to_string from django.urls import reverse_lazy from django.utils.functional import Promise @@ -13,7 +13,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import FormView, ListView, RedirectView, TemplateView from django.views.generic.detail import SingleObjectMixin -from dgsi.forms import CreateKanidmAccountForm +from dgsi.forms import CreateKanidmAccountForm, CreateSelfAccountForm from dgsi.mixins import StaffRequiredMixin from dgsi.models import Bylaws, Service, Statutes, User from shared.kanidm import client @@ -34,7 +34,7 @@ AUTHENTICATED_LINKS: list[Link] = [ ADMIN_LINKS: list[Link] = [ Link( "is-danger", - "dgsi:dgn-create_user", + "dgsi:dgn-create_kanidm_user", _("Créer un nouveau compte Kanidm"), "user-plus", ), @@ -69,13 +69,97 @@ class ProfileView(LoginRequiredMixin, TemplateView): ) +# INFO: We subclass AccessMixin and not LoginRequiredMixin because the way we want to +# use dispatch means that we need to execute the login check anyways. +class CreateSelfAccountView(AccessMixin, SuccessMessageMixin, FormView): + template_name = "dgsi/create_self_account.html" + form_class = CreateSelfAccountForm + success_message = _("Compte DGNum créé avec succès") + success_url = reverse_lazy("dgsi:dgn-profile") + + def dispatch( + self, request: HttpRequest, *args: Any, **kwargs: Any + ) -> HttpResponseBase: + if not request.user.is_authenticated: + return self.handle_no_permission() + + u = User.from_request(request) + + # Check that the user does not already exist + if u.kanidm is not None: + messages.add_message( + request, + messages.WARNING, + _("Vous possédez déjà un compte DGNum !"), + ) + return HttpResponseRedirect(reverse_lazy("dgsi:dgn-profile")) + + # Check that the Statutes and Bylaws have been accepted + if ( + u.accepted_statutes != Statutes.latest() + or u.accepted_bylaws != Bylaws.latest() + ): + messages.add_message( + request, + messages.WARNING, + _("Vous devez accepter les Statuts et le Règlement Intérieur."), + ) + return HttpResponseRedirect(reverse_lazy("dgsi:dgn-legal_documents")) + + return super().dispatch(request, *args, **kwargs) + + @async_to_sync + async def form_valid(self, form): + ttl = 86400 # 24h + + d = form.cleaned_data + u = User.from_request(self.request) + + # Create the base account + await client.person_account_create(u.username, d["displayname"]) + + # Update the information + await client.person_account_update(u.username, mail=[d["mail"]]) + + # FIXME: Will maybe change when kanidm gets its shit together and switches to POST + r = await client.call_get( + f"/v1/person/{u.username}/_credential/_update_intent/{ttl}" + ) + + assert r.data is not None + + token: str = r.data["token"] + link = f"https://sso.dgnum.eu/ui/reset?token={token}" + + # Send an email to the new user with the given email address + EmailMessage( + subject="Réinitialisation de mot de passe DGNum -- DGNum password reset", + body=render_to_string( + "mail/credentials_reset.txt", + context={"link": link}, + ), + from_email="To Be Determined ", + to=[d["mail"]], + headers={"Reply-To": "contact@dgnum.eu"}, + ).send() + + return super().form_valid(form) + + class LegalDocumentsView(LoginRequiredMixin, TemplateView): template_name = "dgsi/legal_documents.html" def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + u = User.from_request(self.request) + statutes = Statutes.latest() + bylaws = Bylaws.latest() + return super().get_context_data( - statutes=Statutes.latest(), - bylaws=Bylaws.latest(), + statutes=statutes, + bylaws=bylaws, + show_message=( + (u.accepted_bylaws != bylaws) or (u.accepted_statutes != statutes) + ), **kwargs, ) @@ -129,7 +213,7 @@ class CreateKanidmAccountView(StaffRequiredMixin, SuccessMessageMixin, FormView) template_name = "dgsi/create_kanidm_account.html" success_message = _("Compte DGNum pour %(displayname)s [%(name)s] créé.") - success_url = reverse_lazy("dgsi:dgn-create_user") + success_url = reverse_lazy("dgsi:dgn-create_kanidm_user") @async_to_sync async def form_valid(self, form):