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
This commit is contained in:
Tom Hubrecht 2024-09-24 15:57:28 +02:00
parent 6c18ec3855
commit 5f0dfff4ae
Signed by: thubrecht
SSH key fingerprint: SHA256:r+nK/SIcWlJ0zFZJGHtlAoRwq1Rm+WcKAm5ADYMoQPc
6 changed files with 141 additions and 11 deletions

View file

@ -33,3 +33,11 @@ class CreateKanidmAccountForm(forms.Form):
help_text=_("Si selectionné, la personne sera ajoutée au groupe dgnum_members"), help_text=_("Si selectionné, la personne sera ajoutée au groupe dgnum_members"),
required=False, 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'"),
)

View file

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h2 class="subtitle">{% trans "Création d'un compte DGNum" %}</h2>
<hr>
<form method="post">
{% csrf_token %}
{% include "bulma/form.html" with form=form %}
<button class="button is-fullwidth mt-6">{% trans "Enregistrer" %}</button>
</form>
{% endblock content %}

View file

@ -6,6 +6,21 @@
<h2 class="subtitle">Documents Légaux</h2> <h2 class="subtitle">Documents Légaux</h2>
<hr> <hr>
{% if user.kanidm is None %}
{% if show_message %}
<div class="notification is-warning is-light has-text-centered">
<b>{% trans "Vous devez accepter les Statuts et le Règlement Intérieur de la DGNum avant de pouvoir créer un compte." %}</b>
</div>
{% else %}
<div class="notification is-primary is-light has-text-centered">
<b>{% trans "Vous n'avez pas encore de compte DGNum, mais vous pouvez désormais en créer un." %}</b>
<br>
<a class="button mt-5 is-light"
href="{% url "dgsi:dgn-create_self_account" %}">{% trans "Poursuivre la création d'un compte DGNum" %}</a>
</div>
{% endif %}
{% endif %}
<br class="my-5"> <br class="my-5">
{% include "_legal_document.html" with document=statutes user_document=user.accepted_statutes title=_("Statuts") accept_question=_("Accepter les statuts") %} {% include "_legal_document.html" with document=statutes user_document=user.accepted_statutes title=_("Statuts") accept_question=_("Accepter les statuts") %}

View file

@ -47,8 +47,11 @@
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="notification px-5 py-5 is-warning is-light has-text-centered is-size-4 mt-6"> <div class="notification is-primary is-light has-text-centered mt-6">
{% trans "Pas de compte DGNum répertorié." %} <b>{% trans "Pas de compte DGNum répertorié." %}</b>
<br>
<a class="button mt-5 is-light"
href="{% url "dgsi:dgn-create_self_account" %}">{% trans "Créer un compte DGNum" %}</a>
</div> </div>
{% endif %} {% endif %}
{% endblock content %} {% endblock content %}

View file

@ -20,10 +20,15 @@ urlpatterns = [
), ),
# Account views # Account views
path("accounts/profile/", views.ProfileView.as_view(), name="dgn-profile"), path("accounts/profile/", views.ProfileView.as_view(), name="dgn-profile"),
path(
"accounts/create/",
views.CreateSelfAccountView.as_view(),
name="dgn-create_self_account",
),
path( path(
"accounts/create-kanidm/", "accounts/create-kanidm/",
views.CreateKanidmAccountView.as_view(), views.CreateKanidmAccountView.as_view(),
name="dgn-create_user", name="dgn-create_kanidm_user",
), ),
path( path(
"accounts/forbidden/", "accounts/forbidden/",
@ -33,7 +38,7 @@ urlpatterns = [
# Services views # Services views
path("services/", views.ServiceListView.as_view(), name="dgn-services"), path("services/", views.ServiceListView.as_view(), name="dgn-services"),
path( path(
"services/redirect/<int:pk>", "services/redirect/<int:pk>/",
views.ServiceRedirectView.as_view(), views.ServiceRedirectView.as_view(),
name="dgn-services_redirect", name="dgn-services_redirect",
), ),

View file

@ -2,10 +2,10 @@ from typing import Any, NamedTuple
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from django.contrib import messages 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.contrib.messages.views import SuccessMessageMixin
from django.core.mail import EmailMessage 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.template.loader import render_to_string
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.functional import Promise 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 import FormView, ListView, RedirectView, TemplateView
from django.views.generic.detail import SingleObjectMixin 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.mixins import StaffRequiredMixin
from dgsi.models import Bylaws, Service, Statutes, User from dgsi.models import Bylaws, Service, Statutes, User
from shared.kanidm import client from shared.kanidm import client
@ -34,7 +34,7 @@ AUTHENTICATED_LINKS: list[Link] = [
ADMIN_LINKS: list[Link] = [ ADMIN_LINKS: list[Link] = [
Link( Link(
"is-danger", "is-danger",
"dgsi:dgn-create_user", "dgsi:dgn-create_kanidm_user",
_("Créer un nouveau compte Kanidm"), _("Créer un nouveau compte Kanidm"),
"user-plus", "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,
_("<b>Vous possédez déjà un compte DGNum !</b>"),
)
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 <dgsi@infra.dgnum.eu>",
to=[d["mail"]],
headers={"Reply-To": "contact@dgnum.eu"},
).send()
return super().form_valid(form)
class LegalDocumentsView(LoginRequiredMixin, TemplateView): class LegalDocumentsView(LoginRequiredMixin, TemplateView):
template_name = "dgsi/legal_documents.html" template_name = "dgsi/legal_documents.html"
def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 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( return super().get_context_data(
statutes=Statutes.latest(), statutes=statutes,
bylaws=Bylaws.latest(), bylaws=bylaws,
show_message=(
(u.accepted_bylaws != bylaws) or (u.accepted_statutes != statutes)
),
**kwargs, **kwargs,
) )
@ -129,7 +213,7 @@ class CreateKanidmAccountView(StaffRequiredMixin, SuccessMessageMixin, FormView)
template_name = "dgsi/create_kanidm_account.html" template_name = "dgsi/create_kanidm_account.html"
success_message = _("Compte DGNum pour %(displayname)s [%(name)s] créé.") 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_to_sync
async def form_valid(self, form): async def form_valid(self, form):