From b63cb8b8c8c5077aff75d6f8763b5eb0b743b9b6 Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Thu, 13 Oct 2022 13:27:51 +0200
Subject: [PATCH 09/10] add a page for users to input their sms registration
 code (#69223)

---
 src/authentic2/forms/registration.py          |  7 ++-
 .../sms_input_registration_code.html          | 17 ++++++
 src/authentic2/urls.py                        |  5 ++
 src/authentic2/views.py                       | 55 +++++++++++++++++++
 4 files changed, 83 insertions(+), 1 deletion(-)
 create mode 100644 src/authentic2/templates/registration/sms_input_registration_code.html

diff --git a/src/authentic2/forms/registration.py b/src/authentic2/forms/registration.py
index 99324039a..2808b9f06 100644
--- a/src/authentic2/forms/registration.py
+++ b/src/authentic2/forms/registration.py
@@ -20,11 +20,12 @@ from django.contrib.auth import get_user_model
 from django.contrib.auth.models import BaseUserManager, Group
 from django.core.exceptions import ValidationError
 from django.core.validators import RegexValidator
+from django.forms import Form
 from django.utils.translation import ugettext
 from django.utils.translation import ugettext_lazy as _
 
 from authentic2.a2_rbac.models import OrganizationalUnit
-from authentic2.forms.fields import CheckPasswordField, NewPasswordField
+from authentic2.forms.fields import CharField, CheckPasswordField, NewPasswordField
 from authentic2.passwords import get_min_password_strength
 
 from .. import app_settings, models
@@ -186,3 +187,7 @@ class RegistrationCompletionForm(RegistrationCompletionFormNoPassword):
                 raise ValidationError(_("The two password fields didn't match."))
             self.instance.set_password(self.cleaned_data['password1'])
         return self.cleaned_data
+
+
+class InputRegistrationCodeForm(Form):
+    registration_code = CharField()
diff --git a/src/authentic2/templates/registration/sms_input_registration_code.html b/src/authentic2/templates/registration/sms_input_registration_code.html
new file mode 100644
index 000000000..82e7c3910
--- /dev/null
+++ b/src/authentic2/templates/registration/sms_input_registration_code.html
@@ -0,0 +1,17 @@
+{% load i18n gadjo %}
+
+{% block page-title %}
+  {{ view.title }}
+{% endblock %}
+
+{% block content %}
+  <form method="post" action=".">
+    <p>{% blocktrans trimmed %}Input your account activation code.{% endblocktrans %}</p>
+    {% csrf_token %}
+    {{ form }}
+    <div class="buttons">
+      <button class="submit-button">{% trans 'Submit' %}</button>
+      <button class="cancel-button" name="cancel" formnovalidate>{% trans "Cancel" %}</button>
+    </div>
+  </form>
+{% endblock %}
diff --git a/src/authentic2/urls.py b/src/authentic2/urls.py
index b23d0b516..c66830511 100644
--- a/src/authentic2/urls.py
+++ b/src/authentic2/urls.py
@@ -114,6 +114,11 @@ urlpatterns = [
         TemplateView.as_view(template_name='registration/registration_closed.html'),
         name='registration_disallowed',
     ),
+    path(
+        'register/input_code/(?P<token>[A-Za-z0-9_ -]+)/',
+        views.input_registration_code,
+        name='input_registration_code',
+    ),
     # Password reset
     re_path(
         r'^password/reset/confirm/(?P<token>[A-Za-z0-9_ -]+)/$',
diff --git a/src/authentic2/views.py b/src/authentic2/views.py
index 81c31f318..8676feead 100644
--- a/src/authentic2/views.py
+++ b/src/authentic2/views.py
@@ -1202,6 +1202,61 @@ class BaseRegistrationView(HomeURLMixin, FormView):
         return context
 
 
+class InputRegistrationCodeView(cbv.ValidateCSRFMixin, FormView):
+    template_name = 'registration/sms_input_registration_code.html'
+    form_class = registration_forms.InputRegistrationCodeForm
+    success_url = '/accounts/'
+    title = _('Account activation')
+
+    def dispatch(self, request, *args, **kwargs):
+        token = kwargs.get('token')
+        try:
+            self.code = models.SMSCode.objects.get(url_token=token)
+        except models.SMSCode.DoesNotExist:
+            return HttpResponseBadRequest(_('Invalid account activation request'))
+        if not self.code.sent:
+            return HttpResponseBadRequest(_('Invalid account activation code'))
+        return super().dispatch(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        if 'cancel' in request.POST:
+            self.code.delete()
+            return utils_misc.redirect(request, reverse('auth_homepage'))
+        return super().post(request, *args, **kwargs)
+
+    def form_valid(self, form):
+        super().form_valid(form)
+        registration_code = form.cleaned_data.pop('registration_code')
+        if self.code.value != registration_code:
+            # TODO ratelimit on erroneous code inputs(?)
+            # (code expires after 120 seconds)
+            form.add_error('registration_code', _('Wrong registration code.'))
+            return self.form_invalid(form)
+        if self.code.expires < timezone.now():
+            form.add_error('registration_code', _('The code has expired.'))
+            return self.form_invalid(form)
+        Lock.lock_identifier(self.code.phone)
+        content = {
+            # TODO missing ou registration management
+            'authentication_method': 'phone',
+            'phone': self.code.phone,
+        }
+        # create token to process final account activation and user-defined attributes
+        token = models.Token.create(
+            kind='registration',
+            content=content,
+            duration=120,
+        )
+        return utils_misc.redirect(
+            # TODO next_url management throughout account creation process
+            self.request,
+            reverse('registration_activate', kwargs={'registration_token': token.uuid}),
+        )
+
+
+input_registration_code = InputRegistrationCodeView.as_view()
+
+
 class RegistrationView(cbv.ValidateCSRFMixin, BaseRegistrationView):
     pass
 
-- 
2.37.2

