We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
feat: Handle error for SSO signup if user does not have email set
## Description

With Google and Apple SSO it seems we have a guarantee that the user will have their email set. This is good for us; the email is a required column in our user table. However, with Microsoft it seems like we do not have this guarantee, which causes us to display an ugly error message in the case where the user does not have an email defined. There are several ways we could handle this:

1) Display an error message that instructs the user to set their email. This is definitely less user friendly, but may be easier to implement.

2) Allow the user to set their email interactively. This is what is done, for example, with Kobo. The idea here would be to set up a signup page as the final signup step. This page would be pre-populated with whatever content we get from Microsoft, but it will allow the user to manually set their email if it is not present.

Any other options you see?


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `terraso_backend/apps/auth/services.py`
Content:
```
1 # Copyright © 2021-2023 Technology Matters
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License as published
5 # by the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU Affero General Public License for more details.
12 #
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see https://www.gnu.org/licenses/.
15 
16 import ipaddress
17 from datetime import timedelta
18 from typing import Any, Optional
19 from urllib.parse import urlparse
20 from uuid import uuid4
21 
22 import httpx
23 import jwt
24 import structlog
25 from django.conf import settings
26 from django.contrib.auth import get_user_model
27 from django.utils import timezone
28 
29 from apps.storage.services import ProfileImageService
30 
31 from .providers import AppleProvider, GoogleProvider, MicrosoftProvider
32 
33 logger = structlog.get_logger(__name__)
34 User = get_user_model()
35 
36 
37 class AccountService:
38     def sign_up_with_google(self, authorization_code):
39         provider = GoogleProvider()
40         tokens = provider.fetch_auth_tokens(authorization_code)
41 
42         if not tokens.is_valid:
43             error_msg = f"Error fetching auth tokens: {tokens.error_description}"
44             logger.error(error_msg)
45             raise Exception(error_msg)
46 
47         return self._persist_user(
48             tokens.open_id.email,
49             first_name=tokens.open_id.given_name,
50             last_name=tokens.open_id.family_name,
51             profile_image_url=tokens.open_id.picture,
52         )
53 
54     def sign_up_with_apple(self, authorization_code, first_name="", last_name=""):
55         provider = AppleProvider()
56         tokens = provider.fetch_auth_tokens(authorization_code)
57 
58         if not tokens.is_valid:
59             error_msg = f"Error fetching auth tokens: {tokens.error_description}"
60             logger.error(error_msg)
61             raise Exception(error_msg)
62 
63         return self._persist_user(tokens.open_id.email, first_name=first_name, last_name=last_name)
64 
65     def sign_up_with_microsoft(self, authorization_code):
66         provider = MicrosoftProvider()
67         tokens = provider.fetch_auth_tokens(authorization_code)
68         if not tokens.is_valid:
69             error_msg = f"Error fetching auth tokens: {tokens.error_description}"
70             logger.error(error_msg)
71             raise Exception(error_msg)
72         return self._persist_user(
73             tokens.open_id.email,
74             first_name=tokens.open_id.given_name,
75             last_name=tokens.open_id.family_name,
76             profile_image_url=tokens.open_id.picture,
77         )
78 
79     def _persist_user(self, email, first_name="", last_name="", profile_image_url=None):
80         user, created = User.objects.get_or_create(email=email)
81 
82         self._update_profile_image(user, profile_image_url)
83 
84         if not created:
85             return user, False
86 
87         update_name = first_name or last_name
88 
89         if first_name:
90             user.first_name = first_name
91 
92         if last_name:
93             user.last_name = last_name
94 
95         if update_name:
96             user.save()
97 
98         return user, True
99 
100     def _update_profile_image(self, user, profile_image_url):
101         if not profile_image_url:
102             return
103 
104         profile_image_service = ProfileImageService()
105         user_id = str(user.id)
106 
107         try:
108             user.profile_image = profile_image_service.upload_url(user_id, profile_image_url)
109             user.save()
110         except Exception:
111             logger.exception("Failed to upload profile image. User ID: {}".format(user_id))
112 
113 
114 class JWTService:
115     JWT_SECRET = settings.JWT_SECRET
116     JWT_ALGORITHM = settings.JWT_ALGORITHM
117     JWT_ACCESS_EXP_DELTA_SECONDS = settings.JWT_ACCESS_EXP_DELTA_SECONDS
118     JWT_REFRESH_EXP_DELTA_SECONDS = settings.JWT_REFRESH_EXP_DELTA_SECONDS
119     JWT_ISS = settings.JWT_ISS
120 
121     def create_access_token(self, user):
122         payload = self._get_base_payload(user)
123         payload["exp"] = timezone.now() + timedelta(seconds=self.JWT_ACCESS_EXP_DELTA_SECONDS)
124 
125         return jwt.encode(payload, self.JWT_SECRET, algorithm=self.JWT_ALGORITHM)
126 
127     def create_refresh_token(self, user):
128         payload = self._get_base_payload(user)
129         payload["exp"] = timezone.now() + timedelta(seconds=self.JWT_REFRESH_EXP_DELTA_SECONDS)
130 
131         return jwt.encode(payload, self.JWT_SECRET, algorithm=self.JWT_ALGORITHM)
132 
133     def verify_token(self, token):
134         return jwt.decode(token, self.JWT_SECRET, algorithms=self.JWT_ALGORITHM)
135 
136     def _get_base_payload(self, user):
137         return {
138             "iss": self.JWT_ISS,
139             "iat": timezone.now(),
140             "sub": str(user.id),
141             "jti": uuid4().hex,
142             "email": user.email,
143         }
144 
145 
146 class PlausibleService:
147     """Service for making API calls to plausible service.
148 
149     See documentation at https://plausible.io/docs/events-api .
150     """
151 
152     PLAUSIBLE_URL = settings.PLAUSIBLE_URL
153     FRONTEND_URL = settings.WEB_CLIENT_URL
154     # fake URL here, because there is no real "signup" URL
155     # see Plausible API docs for "url" param
156     EVENT_URL = f"{FRONTEND_URL}/signup"
157 
158     @staticmethod
159     def _prepare_headers(user_agent: str, ip_address: str) -> dict[str, str]:
160         return {
161             "User-Agent": user_agent,
162             "X-Forwarded-For": ip_address,
163             "Content-Type": "application/json",
164         }
165 
166     @classmethod
167     def _prepare_body_params(
168         cls, event_name: str, event_url: str, referrer: str, props: Optional[dict[str, Any]]
169     ):
170         return {
171             "domain": urlparse(cls.FRONTEND_URL).hostname,
172             "name": event_name,
173             "url": event_url,
174             "referrer": referrer,
175             "props": props,
176         }
177 
178     @staticmethod
179     def _get_first_ip_address(string: str):
180         addresses = string.split(",")
181         for addr in addresses:
182             try:
183                 ip_address = ipaddress.ip_address(addr)
184                 break
185             except ValueError:
186                 pass
187         else:
188             # we only get to this branch if we never break
189             # i.e. none of the candidates are valid ip addresses
190             return None
191         return str(ip_address)
192 
193     def track_event(
194         self,
195         event_name: str,
196         user_agent: str,
197         ip_address: str,
198         event_url: str,
199         props: Optional[dict[str, Any]] = None,
200         referrer: str = "",
201     ) -> None:
202         """Send a tracking event to Plausible through the HTTP API.
203         Throws exception if not succesful."""
204         headers = self._prepare_headers(user_agent, ip_address)
205         data = self._prepare_body_params(event_name, event_url, referrer, props)
206         resp = httpx.post(self.PLAUSIBLE_URL, headers=headers, json=data)
207 
208         resp.raise_for_status()
209 
210     def track_signup(self, auth_provider: str, req) -> None:
211         """Track a successful signup. Include information on which service was used for signup."""
212         event_name = "User signup"
213         if "user-agent" not in req.headers:
214             logger.error("During signup tracking, request missing header 'user-agent'")
215             return
216         user_agent = req.headers["user-agent"]
217         # here we just assume we are testing locally if 'x-forwarded-for' header is not present
218         # this is a mandatory header for the Plausible API, see docs for details
219         ip_address = "127.0.0.1"
220         if "x-forwarded-for" in req.headers:
221             ip_address = self._get_first_ip_address(req.headers["x-forwarded-for"])
222             if not ip_address:
223                 logger.error(
224                     "During signup tracking, request header 'x-forwarded-for' was set,"
225                     " but no valid ip addresses were found"
226                 )
227                 return
228         props = {"service": auth_provider}
229         self.track_event(event_name, user_agent, ip_address, self.EVENT_URL, props)
230
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/terraso_backend/apps/auth/services.py b/terraso_backend/apps/auth/services.py
--- a/terraso_backend/apps/auth/services.py
+++ b/terraso_backend/apps/auth/services.py
@@ -77,6 +77,10 @@
         )
 
     def _persist_user(self, email, first_name="", last_name="", profile_image_url=None):
+        if not email:
+            # it is possible for the email not to be set, notably with Microsoft
+            # here throw a more descriptive error message
+            raise ValueError("Could not create account, user email is empty")
         user, created = User.objects.get_or_create(email=email)
 
         self._update_profile_image(user, profile_image_url)

{"golden_diff": "diff --git a/terraso_backend/apps/auth/services.py b/terraso_backend/apps/auth/services.py\n--- a/terraso_backend/apps/auth/services.py\n+++ b/terraso_backend/apps/auth/services.py\n@@ -77,6 +77,10 @@\n         )\n \n     def _persist_user(self, email, first_name=\"\", last_name=\"\", profile_image_url=None):\n+        if not email:\n+            # it is possible for the email not to be set, notably with Microsoft\n+            # here throw a more descriptive error message\n+            raise ValueError(\"Could not create account, user email is empty\")\n         user, created = User.objects.get_or_create(email=email)\n \n         self._update_profile_image(user, profile_image_url)\n", "issue": "feat: Handle error for SSO signup if user does not have email set\n## Description\r\n\r\nWith Google and Apple SSO it seems we have a guarantee that the user will have their email set. This is good for us; the email is a required column in our user table. However, with Microsoft it seems like we do not have this guarantee, which causes us to display an ugly error message in the case where the user does not have an email defined. There are several ways we could handle this:\r\n\r\n1) Display an error message that instructs the user to set their email. This is definitely less user friendly, but may be easier to implement.\r\n\r\n2) Allow the user to set their email interactively. This is what is done, for example, with Kobo. The idea here would be to set up a signup page as the final signup step. This page would be pre-populated with whatever content we get from Microsoft, but it will allow the user to manually set their email if it is not present.\r\n\r\nAny other options you see?\r\n\n", "before_files": [{"content": "# Copyright \u00a9 2021-2023 Technology Matters\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU Affero General Public License as published\n# by the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n# GNU Affero General Public License for more details.\n#\n# You should have received a copy of the GNU Affero General Public License\n# along with this program. If not, see https://www.gnu.org/licenses/.\n\nimport ipaddress\nfrom datetime import timedelta\nfrom typing import Any, Optional\nfrom urllib.parse import urlparse\nfrom uuid import uuid4\n\nimport httpx\nimport jwt\nimport structlog\nfrom django.conf import settings\nfrom django.contrib.auth import get_user_model\nfrom django.utils import timezone\n\nfrom apps.storage.services import ProfileImageService\n\nfrom .providers import AppleProvider, GoogleProvider, MicrosoftProvider\n\nlogger = structlog.get_logger(__name__)\nUser = get_user_model()\n\n\nclass AccountService:\n    def sign_up_with_google(self, authorization_code):\n        provider = GoogleProvider()\n        tokens = provider.fetch_auth_tokens(authorization_code)\n\n        if not tokens.is_valid:\n            error_msg = f\"Error fetching auth tokens: {tokens.error_description}\"\n            logger.error(error_msg)\n            raise Exception(error_msg)\n\n        return self._persist_user(\n            tokens.open_id.email,\n            first_name=tokens.open_id.given_name,\n            last_name=tokens.open_id.family_name,\n            profile_image_url=tokens.open_id.picture,\n        )\n\n    def sign_up_with_apple(self, authorization_code, first_name=\"\", last_name=\"\"):\n        provider = AppleProvider()\n        tokens = provider.fetch_auth_tokens(authorization_code)\n\n        if not tokens.is_valid:\n            error_msg = f\"Error fetching auth tokens: {tokens.error_description}\"\n            logger.error(error_msg)\n            raise Exception(error_msg)\n\n        return self._persist_user(tokens.open_id.email, first_name=first_name, last_name=last_name)\n\n    def sign_up_with_microsoft(self, authorization_code):\n        provider = MicrosoftProvider()\n        tokens = provider.fetch_auth_tokens(authorization_code)\n        if not tokens.is_valid:\n            error_msg = f\"Error fetching auth tokens: {tokens.error_description}\"\n            logger.error(error_msg)\n            raise Exception(error_msg)\n        return self._persist_user(\n            tokens.open_id.email,\n            first_name=tokens.open_id.given_name,\n            last_name=tokens.open_id.family_name,\n            profile_image_url=tokens.open_id.picture,\n        )\n\n    def _persist_user(self, email, first_name=\"\", last_name=\"\", profile_image_url=None):\n        user, created = User.objects.get_or_create(email=email)\n\n        self._update_profile_image(user, profile_image_url)\n\n        if not created:\n            return user, False\n\n        update_name = first_name or last_name\n\n        if first_name:\n            user.first_name = first_name\n\n        if last_name:\n            user.last_name = last_name\n\n        if update_name:\n            user.save()\n\n        return user, True\n\n    def _update_profile_image(self, user, profile_image_url):\n        if not profile_image_url:\n            return\n\n        profile_image_service = ProfileImageService()\n        user_id = str(user.id)\n\n        try:\n            user.profile_image = profile_image_service.upload_url(user_id, profile_image_url)\n            user.save()\n        except Exception:\n            logger.exception(\"Failed to upload profile image. User ID: {}\".format(user_id))\n\n\nclass JWTService:\n    JWT_SECRET = settings.JWT_SECRET\n    JWT_ALGORITHM = settings.JWT_ALGORITHM\n    JWT_ACCESS_EXP_DELTA_SECONDS = settings.JWT_ACCESS_EXP_DELTA_SECONDS\n    JWT_REFRESH_EXP_DELTA_SECONDS = settings.JWT_REFRESH_EXP_DELTA_SECONDS\n    JWT_ISS = settings.JWT_ISS\n\n    def create_access_token(self, user):\n        payload = self._get_base_payload(user)\n        payload[\"exp\"] = timezone.now() + timedelta(seconds=self.JWT_ACCESS_EXP_DELTA_SECONDS)\n\n        return jwt.encode(payload, self.JWT_SECRET, algorithm=self.JWT_ALGORITHM)\n\n    def create_refresh_token(self, user):\n        payload = self._get_base_payload(user)\n        payload[\"exp\"] = timezone.now() + timedelta(seconds=self.JWT_REFRESH_EXP_DELTA_SECONDS)\n\n        return jwt.encode(payload, self.JWT_SECRET, algorithm=self.JWT_ALGORITHM)\n\n    def verify_token(self, token):\n        return jwt.decode(token, self.JWT_SECRET, algorithms=self.JWT_ALGORITHM)\n\n    def _get_base_payload(self, user):\n        return {\n            \"iss\": self.JWT_ISS,\n            \"iat\": timezone.now(),\n            \"sub\": str(user.id),\n            \"jti\": uuid4().hex,\n            \"email\": user.email,\n        }\n\n\nclass PlausibleService:\n    \"\"\"Service for making API calls to plausible service.\n\n    See documentation at https://plausible.io/docs/events-api .\n    \"\"\"\n\n    PLAUSIBLE_URL = settings.PLAUSIBLE_URL\n    FRONTEND_URL = settings.WEB_CLIENT_URL\n    # fake URL here, because there is no real \"signup\" URL\n    # see Plausible API docs for \"url\" param\n    EVENT_URL = f\"{FRONTEND_URL}/signup\"\n\n    @staticmethod\n    def _prepare_headers(user_agent: str, ip_address: str) -> dict[str, str]:\n        return {\n            \"User-Agent\": user_agent,\n            \"X-Forwarded-For\": ip_address,\n            \"Content-Type\": \"application/json\",\n        }\n\n    @classmethod\n    def _prepare_body_params(\n        cls, event_name: str, event_url: str, referrer: str, props: Optional[dict[str, Any]]\n    ):\n        return {\n            \"domain\": urlparse(cls.FRONTEND_URL).hostname,\n            \"name\": event_name,\n            \"url\": event_url,\n            \"referrer\": referrer,\n            \"props\": props,\n        }\n\n    @staticmethod\n    def _get_first_ip_address(string: str):\n        addresses = string.split(\",\")\n        for addr in addresses:\n            try:\n                ip_address = ipaddress.ip_address(addr)\n                break\n            except ValueError:\n                pass\n        else:\n            # we only get to this branch if we never break\n            # i.e. none of the candidates are valid ip addresses\n            return None\n        return str(ip_address)\n\n    def track_event(\n        self,\n        event_name: str,\n        user_agent: str,\n        ip_address: str,\n        event_url: str,\n        props: Optional[dict[str, Any]] = None,\n        referrer: str = \"\",\n    ) -> None:\n        \"\"\"Send a tracking event to Plausible through the HTTP API.\n        Throws exception if not succesful.\"\"\"\n        headers = self._prepare_headers(user_agent, ip_address)\n        data = self._prepare_body_params(event_name, event_url, referrer, props)\n        resp = httpx.post(self.PLAUSIBLE_URL, headers=headers, json=data)\n\n        resp.raise_for_status()\n\n    def track_signup(self, auth_provider: str, req) -> None:\n        \"\"\"Track a successful signup. Include information on which service was used for signup.\"\"\"\n        event_name = \"User signup\"\n        if \"user-agent\" not in req.headers:\n            logger.error(\"During signup tracking, request missing header 'user-agent'\")\n            return\n        user_agent = req.headers[\"user-agent\"]\n        # here we just assume we are testing locally if 'x-forwarded-for' header is not present\n        # this is a mandatory header for the Plausible API, see docs for details\n        ip_address = \"127.0.0.1\"\n        if \"x-forwarded-for\" in req.headers:\n            ip_address = self._get_first_ip_address(req.headers[\"x-forwarded-for\"])\n            if not ip_address:\n                logger.error(\n                    \"During signup tracking, request header 'x-forwarded-for' was set,\"\n                    \" but no valid ip addresses were found\"\n                )\n                return\n        props = {\"service\": auth_provider}\n        self.track_event(event_name, user_agent, ip_address, self.EVENT_URL, props)\n", "path": "terraso_backend/apps/auth/services.py"}], "after_files": [{"content": "# Copyright \u00a9 2021-2023 Technology Matters\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU Affero General Public License as published\n# by the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n# GNU Affero General Public License for more details.\n#\n# You should have received a copy of the GNU Affero General Public License\n# along with this program. If not, see https://www.gnu.org/licenses/.\n\nimport ipaddress\nfrom datetime import timedelta\nfrom typing import Any, Optional\nfrom urllib.parse import urlparse\nfrom uuid import uuid4\n\nimport httpx\nimport jwt\nimport structlog\nfrom django.conf import settings\nfrom django.contrib.auth import get_user_model\nfrom django.utils import timezone\n\nfrom apps.storage.services import ProfileImageService\n\nfrom .providers import AppleProvider, GoogleProvider, MicrosoftProvider\n\nlogger = structlog.get_logger(__name__)\nUser = get_user_model()\n\n\nclass AccountService:\n    def sign_up_with_google(self, authorization_code):\n        provider = GoogleProvider()\n        tokens = provider.fetch_auth_tokens(authorization_code)\n\n        if not tokens.is_valid:\n            error_msg = f\"Error fetching auth tokens: {tokens.error_description}\"\n            logger.error(error_msg)\n            raise Exception(error_msg)\n\n        return self._persist_user(\n            tokens.open_id.email,\n            first_name=tokens.open_id.given_name,\n            last_name=tokens.open_id.family_name,\n            profile_image_url=tokens.open_id.picture,\n        )\n\n    def sign_up_with_apple(self, authorization_code, first_name=\"\", last_name=\"\"):\n        provider = AppleProvider()\n        tokens = provider.fetch_auth_tokens(authorization_code)\n\n        if not tokens.is_valid:\n            error_msg = f\"Error fetching auth tokens: {tokens.error_description}\"\n            logger.error(error_msg)\n            raise Exception(error_msg)\n\n        return self._persist_user(tokens.open_id.email, first_name=first_name, last_name=last_name)\n\n    def sign_up_with_microsoft(self, authorization_code):\n        provider = MicrosoftProvider()\n        tokens = provider.fetch_auth_tokens(authorization_code)\n        if not tokens.is_valid:\n            error_msg = f\"Error fetching auth tokens: {tokens.error_description}\"\n            logger.error(error_msg)\n            raise Exception(error_msg)\n        return self._persist_user(\n            tokens.open_id.email,\n            first_name=tokens.open_id.given_name,\n            last_name=tokens.open_id.family_name,\n            profile_image_url=tokens.open_id.picture,\n        )\n\n    def _persist_user(self, email, first_name=\"\", last_name=\"\", profile_image_url=None):\n        if not email:\n            # it is possible for the email not to be set, notably with Microsoft\n            # here throw a more descriptive error message\n            raise ValueError(\"Could not create account, user email is empty\")\n        user, created = User.objects.get_or_create(email=email)\n\n        self._update_profile_image(user, profile_image_url)\n\n        if not created:\n            return user, False\n\n        update_name = first_name or last_name\n\n        if first_name:\n            user.first_name = first_name\n\n        if last_name:\n            user.last_name = last_name\n\n        if update_name:\n            user.save()\n\n        return user, True\n\n    def _update_profile_image(self, user, profile_image_url):\n        if not profile_image_url:\n            return\n\n        profile_image_service = ProfileImageService()\n        user_id = str(user.id)\n\n        try:\n            user.profile_image = profile_image_service.upload_url(user_id, profile_image_url)\n            user.save()\n        except Exception:\n            logger.exception(\"Failed to upload profile image. User ID: {}\".format(user_id))\n\n\nclass JWTService:\n    JWT_SECRET = settings.JWT_SECRET\n    JWT_ALGORITHM = settings.JWT_ALGORITHM\n    JWT_ACCESS_EXP_DELTA_SECONDS = settings.JWT_ACCESS_EXP_DELTA_SECONDS\n    JWT_REFRESH_EXP_DELTA_SECONDS = settings.JWT_REFRESH_EXP_DELTA_SECONDS\n    JWT_ISS = settings.JWT_ISS\n\n    def create_access_token(self, user):\n        payload = self._get_base_payload(user)\n        payload[\"exp\"] = timezone.now() + timedelta(seconds=self.JWT_ACCESS_EXP_DELTA_SECONDS)\n\n        return jwt.encode(payload, self.JWT_SECRET, algorithm=self.JWT_ALGORITHM)\n\n    def create_refresh_token(self, user):\n        payload = self._get_base_payload(user)\n        payload[\"exp\"] = timezone.now() + timedelta(seconds=self.JWT_REFRESH_EXP_DELTA_SECONDS)\n\n        return jwt.encode(payload, self.JWT_SECRET, algorithm=self.JWT_ALGORITHM)\n\n    def verify_token(self, token):\n        return jwt.decode(token, self.JWT_SECRET, algorithms=self.JWT_ALGORITHM)\n\n    def _get_base_payload(self, user):\n        return {\n            \"iss\": self.JWT_ISS,\n            \"iat\": timezone.now(),\n            \"sub\": str(user.id),\n            \"jti\": uuid4().hex,\n            \"email\": user.email,\n        }\n\n\nclass PlausibleService:\n    \"\"\"Service for making API calls to plausible service.\n\n    See documentation at https://plausible.io/docs/events-api .\n    \"\"\"\n\n    PLAUSIBLE_URL = settings.PLAUSIBLE_URL\n    FRONTEND_URL = settings.WEB_CLIENT_URL\n    # fake URL here, because there is no real \"signup\" URL\n    # see Plausible API docs for \"url\" param\n    EVENT_URL = f\"{FRONTEND_URL}/signup\"\n\n    @staticmethod\n    def _prepare_headers(user_agent: str, ip_address: str) -> dict[str, str]:\n        return {\n            \"User-Agent\": user_agent,\n            \"X-Forwarded-For\": ip_address,\n            \"Content-Type\": \"application/json\",\n        }\n\n    @classmethod\n    def _prepare_body_params(\n        cls, event_name: str, event_url: str, referrer: str, props: Optional[dict[str, Any]]\n    ):\n        return {\n            \"domain\": urlparse(cls.FRONTEND_URL).hostname,\n            \"name\": event_name,\n            \"url\": event_url,\n            \"referrer\": referrer,\n            \"props\": props,\n        }\n\n    @staticmethod\n    def _get_first_ip_address(string: str):\n        addresses = string.split(\",\")\n        for addr in addresses:\n            try:\n                ip_address = ipaddress.ip_address(addr)\n                break\n            except ValueError:\n                pass\n        else:\n            # we only get to this branch if we never break\n            # i.e. none of the candidates are valid ip addresses\n            return None\n        return str(ip_address)\n\n    def track_event(\n        self,\n        event_name: str,\n        user_agent: str,\n        ip_address: str,\n        event_url: str,\n        props: Optional[dict[str, Any]] = None,\n        referrer: str = \"\",\n    ) -> None:\n        \"\"\"Send a tracking event to Plausible through the HTTP API.\n        Throws exception if not succesful.\"\"\"\n        headers = self._prepare_headers(user_agent, ip_address)\n        data = self._prepare_body_params(event_name, event_url, referrer, props)\n        resp = httpx.post(self.PLAUSIBLE_URL, headers=headers, json=data)\n\n        resp.raise_for_status()\n\n    def track_signup(self, auth_provider: str, req) -> None:\n        \"\"\"Track a successful signup. Include information on which service was used for signup.\"\"\"\n        event_name = \"User signup\"\n        if \"user-agent\" not in req.headers:\n            logger.error(\"During signup tracking, request missing header 'user-agent'\")\n            return\n        user_agent = req.headers[\"user-agent\"]\n        # here we just assume we are testing locally if 'x-forwarded-for' header is not present\n        # this is a mandatory header for the Plausible API, see docs for details\n        ip_address = \"127.0.0.1\"\n        if \"x-forwarded-for\" in req.headers:\n            ip_address = self._get_first_ip_address(req.headers[\"x-forwarded-for\"])\n            if not ip_address:\n                logger.error(\n                    \"During signup tracking, request header 'x-forwarded-for' was set,\"\n                    \" but no valid ip addresses were found\"\n                )\n                return\n        props = {\"service\": auth_provider}\n        self.track_event(event_name, user_agent, ip_address, self.EVENT_URL, props)\n", "path": "terraso_backend/apps/auth/services.py"}]}