From 0466b9fff6b6649b70906f069c8f6b48f20b18c9 Mon Sep 17 00:00:00 2001
From: Valentin Rigal <rigal@teklia.com>
Date: Wed, 12 Jun 2024 13:13:05 +0000
Subject: [PATCH] RQ task to send a verification email

---
 arkindex/project/config.py                    |  1 +
 .../tests/config_samples/defaults.yaml        |  1 +
 .../project/tests/config_samples/errors.yaml  |  1 +
 .../tests/config_samples/expected_errors.yaml |  1 +
 .../tests/config_samples/override.yaml        |  5 +--
 arkindex/project/triggers.py                  |  8 +++++
 arkindex/users/api.py                         | 29 ++---------------
 arkindex/users/tasks.py                       | 32 +++++++++++++++++++
 arkindex/users/tests/test_registration.py     |  9 ++++--
 9 files changed, 56 insertions(+), 31 deletions(-)
 create mode 100644 arkindex/users/tasks.py

diff --git a/arkindex/project/config.py b/arkindex/project/config.py
index 81315b5b4c..338b50998e 100644
--- a/arkindex/project/config.py
+++ b/arkindex/project/config.py
@@ -150,6 +150,7 @@ def get_settings_parser(base_dir):
     job_timeouts_parser.add_option("notify_process_completion", type=int, default=120)
     # Task execution in RQ timeouts after 10 hours by default
     job_timeouts_parser.add_option("task", type=int, default=36000)
+    job_timeouts_parser.add_option("send_verification_email", type=int, default=120)
 
     csrf_parser = parser.add_subparser("csrf", default={})
     csrf_parser.add_option("cookie_name", type=str, default="arkindex.csrf")
diff --git a/arkindex/project/tests/config_samples/defaults.yaml b/arkindex/project/tests/config_samples/defaults.yaml
index 59651795f2..96f45ad2c0 100644
--- a/arkindex/project/tests/config_samples/defaults.yaml
+++ b/arkindex/project/tests/config_samples/defaults.yaml
@@ -61,6 +61,7 @@ job_timeouts:
   notify_process_completion: 120
   process_delete: 3600
   reindex_corpus: 7200
+  send_verification_email: 120
   task: 36000
   worker_results_delete: 3600
 jwt_signing_key: null
diff --git a/arkindex/project/tests/config_samples/errors.yaml b/arkindex/project/tests/config_samples/errors.yaml
index ba921e1eb7..c4cbfe7333 100644
--- a/arkindex/project/tests/config_samples/errors.yaml
+++ b/arkindex/project/tests/config_samples/errors.yaml
@@ -44,6 +44,7 @@ job_timeouts:
   reindex_corpus: {}
   task: ''
   worker_results_delete: null
+  send_verification_email: lol
 jwt_signing_key: null
 local_imageserver_id: 1
 metrics_port: 12
diff --git a/arkindex/project/tests/config_samples/expected_errors.yaml b/arkindex/project/tests/config_samples/expected_errors.yaml
index d2a208c675..c070fed5df 100644
--- a/arkindex/project/tests/config_samples/expected_errors.yaml
+++ b/arkindex/project/tests/config_samples/expected_errors.yaml
@@ -29,6 +29,7 @@ job_timeouts:
   reindex_corpus: "int() argument must be a string, a bytes-like object or a real number, not 'dict'"
   task: "invalid literal for int() with base 10: ''"
   worker_results_delete: "int() argument must be a string, a bytes-like object or a real number, not 'NoneType'"
+  send_verification_email: "invalid literal for int() with base 10: 'lol'"
 ponos:
   artifact_max_size: cannot convert float NaN to integer
   task_expiry: "invalid literal for int() with base 10: 'zero'"
diff --git a/arkindex/project/tests/config_samples/override.yaml b/arkindex/project/tests/config_samples/override.yaml
index f0fb66517e..1e4700a47e 100644
--- a/arkindex/project/tests/config_samples/override.yaml
+++ b/arkindex/project/tests/config_samples/override.yaml
@@ -75,8 +75,9 @@ job_timeouts:
   notify_process_completion: 6
   process_delete: 7
   reindex_corpus: 8
-  task: 9
-  worker_results_delete: 10
+  send_verification_email: 9
+  task: 10
+  worker_results_delete: 11
 jwt_signing_key: deadbeef
 local_imageserver_id: 45
 metrics_port: 4242
diff --git a/arkindex/project/triggers.py b/arkindex/project/triggers.py
index 2bd186ef97..9db62d1b7a 100644
--- a/arkindex/project/triggers.py
+++ b/arkindex/project/triggers.py
@@ -16,6 +16,8 @@ from arkindex.ponos.models import State, Task
 from arkindex.process import tasks as process_tasks
 from arkindex.process.models import Process, WorkerActivityState, WorkerConfiguration, WorkerRun, WorkerVersion
 from arkindex.training.models import ModelVersion
+from arkindex.users import tasks as user_tasks
+from arkindex.users.models import User
 
 
 def corpus_delete(corpus: Union[Corpus, UUID, str], user_id: Optional[int] = None) -> None:
@@ -259,3 +261,9 @@ def schedule_tasks(process: Process, run: int):
         if parent_job:
             kwargs["depends_on"] = Dependency(jobs=[parent_job], allow_failure=True)
         parent_job = ponos_tasks.run_task_rq.delay(task, **kwargs)
+
+
+def send_verification_email(user: User):
+    """Send validation email to an user"""
+    assert user.verified_email is False, "Only non verified users can receive a verification email"
+    user_tasks.send_verification_email.delay(user)
diff --git a/arkindex/users/api.py b/arkindex/users/api.py
index 0d00c159c2..8731b4ec5d 100644
--- a/arkindex/users/api.py
+++ b/arkindex/users/api.py
@@ -1,13 +1,9 @@
 import logging
-import urllib.parse
 
 from django.conf import settings
 from django.contrib.auth import login, logout
 from django.contrib.auth.forms import PasswordResetForm
 from django.contrib.auth.tokens import default_token_generator
-from django.core.mail import send_mail
-from django.template.loader import render_to_string
-from django.urls import reverse
 from django_rq.queues import get_queue
 from django_rq.settings import QUEUES
 from drf_spectacular.types import OpenApiTypes
@@ -19,6 +15,7 @@ from rest_framework.response import Response
 from rq.job import JobStatus
 
 from arkindex.project.permissions import IsAuthenticatedOrReadOnly, IsVerified
+from arkindex.project.triggers import send_verification_email
 from arkindex.users.models import User
 from arkindex.users.serializers import (
     EmailLoginSerializer,
@@ -97,29 +94,7 @@ class UserCreate(CreateAPIView):
         user = serializer.save()
         login(self.request, user)
 
-        activation_url = "{}?{}".format(
-            self.request.build_absolute_uri(reverse("frontend-verify-email")),
-            urllib.parse.urlencode({
-                "email": user.email,
-                "token": default_token_generator.make_token(user),
-            }),
-        )
-        sent = send_mail(
-            # Subject cannot have any newlines
-            subject="".join(render_to_string("registration/verification_email_subject.txt").splitlines()),
-            message=render_to_string(
-                "registration/verification_email.html",
-                context={
-                    "url": activation_url,
-                },
-                request=self.request,
-            ),
-            from_email=None,
-            recipient_list=[user.email],
-            fail_silently=True,
-        )
-        if sent == 0:
-            logger.error(f"Failed to send registration email to {user.email}")
+        send_verification_email(user)
 
         return Response(UserSerializer(user).data, status=status.HTTP_201_CREATED)
 
diff --git a/arkindex/users/tasks.py b/arkindex/users/tasks.py
new file mode 100644
index 0000000000..4ca7e257d3
--- /dev/null
+++ b/arkindex/users/tasks.py
@@ -0,0 +1,32 @@
+import urllib
+
+from django.conf import settings
+from django.contrib.auth.tokens import default_token_generator
+from django.core.mail import send_mail
+from django.template.loader import render_to_string
+from django.urls import reverse
+from django_rq import job
+
+from arkindex.users.models import User
+
+
+@job("default", timeout=settings.RQ_TIMEOUTS["send_verification_email"])
+def send_verification_email(user: User):
+    activation_url = "{}?{}".format(
+        urllib.parse.urljoin(settings.PUBLIC_HOSTNAME, reverse("frontend-verify-email")),
+        urllib.parse.urlencode({
+            "email": user.email,
+            "token": default_token_generator.make_token(user),
+        }),
+    )
+    send_mail(
+        # Subject cannot have any newlines
+        subject="".join(render_to_string("registration/verification_email_subject.txt").splitlines()),
+        message=render_to_string(
+            "registration/verification_email.html",
+            context={"url": activation_url},
+        ),
+        from_email=None,
+        recipient_list=[user.email],
+        fail_silently=False,
+    )
diff --git a/arkindex/users/tests/test_registration.py b/arkindex/users/tests/test_registration.py
index c6278a177b..07ec0201b8 100644
--- a/arkindex/users/tests/test_registration.py
+++ b/arkindex/users/tests/test_registration.py
@@ -11,6 +11,7 @@ from arkindex.documents.models import Corpus
 from arkindex.project.default_corpus import DEFAULT_CORPUS_TYPES
 from arkindex.project.tests import FixtureAPITestCase
 from arkindex.users.models import Group, Right, Role, Scope, User
+from arkindex.users.tasks import send_verification_email
 
 
 class TestRegistration(FixtureAPITestCase):
@@ -319,7 +320,9 @@ class TestRegistration(FixtureAPITestCase):
             "__all__": ["There is no user with this email or the token is invalid."]
         })
 
-    def test_create_no_password(self):
+    @patch("arkindex.users.tasks.send_verification_email.delay")
+    def test_create_no_password(self, async_verification_email):
+        async_verification_email.side_effect = send_verification_email
         response = self.client.post(
             reverse("api:user-new"),
             data={"display_name": "New user", "email": "newuser@example.com"},
@@ -334,7 +337,9 @@ class TestRegistration(FixtureAPITestCase):
         self.assertEqual(len(mail.outbox), 1)
         self.assertEqual(mail.outbox[0].to, ["newuser@example.com"])
 
-    def test_create(self):
+    @patch("arkindex.users.tasks.send_verification_email.delay")
+    def test_create(self, async_verification_email):
+        async_verification_email.side_effect = send_verification_email
         response = self.client.post(
             reverse("api:user-new"),
             data={"display_name": "New user", "email": "newuser@example.com", "password": "myVerySecretPassword"},
-- 
GitLab