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"),
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>
<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">
{% 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 %}
</div>
{% else %}
<div class="notification px-5 py-5 is-warning is-light has-text-centered is-size-4 mt-6">
{% trans "Pas de compte DGNum répertorié." %}
<div class="notification is-primary is-light has-text-centered mt-6">
<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>
{% endif %}
{% endblock content %}

View file

@ -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/<int:pk>",
"services/redirect/<int:pk>/",
views.ServiceRedirectView.as_view(),
name="dgn-services_redirect",
),

View file

@ -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,
_("<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):
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):