From a3d6ab644d96bf58b36ed6dea71a7701b67d0326 Mon Sep 17 00:00:00 2001 From: groovecoder Date: Thu, 16 May 2024 14:04:18 -0500 Subject: [PATCH 1/8] for MPP-3817: prevent Relay api and email operations when user.is_active = True --- api/authentication.py | 6 ++++++ api/tests/authentication_tests.py | 21 +++++++++++++++++++++ api/tests/emails_views_tests.py | 18 ++++++++++++++++++ emails/models.py | 12 ++++++++++++ emails/tests/views_tests.py | 11 +++++++++++ emails/views.py | 5 +++++ privaterelay/pending_locales/en/pending.ftl | 1 + 7 files changed, 74 insertions(+) diff --git a/api/authentication.py b/api/authentication.py index 203ef7660c..b8e4971a14 100644 --- a/api/authentication.py +++ b/api/authentication.py @@ -148,6 +148,12 @@ def authenticate(self, request): ) user = sa.user + if not user.is_active: + raise PermissionDenied( + "Authenticated user does not have an active Relay account." + " Have they been deactivated?" + ) + if user: return (user, token) else: diff --git a/api/tests/authentication_tests.py b/api/tests/authentication_tests.py index 90a86aa63a..3cf22a76d6 100644 --- a/api/tests/authentication_tests.py +++ b/api/tests/authentication_tests.py @@ -351,6 +351,27 @@ def test_200_resp_from_fxa_no_matching_user_raises_APIException(self) -> None: response = client.get("/api/v1/relayaddresses/") assert responses.assert_call_count(self.fxa_verify_path, 1) is True + @responses.activate + def test_200_resp_from_fxa_inactive_Relay_user_raises_APIException(self) -> None: + sa: SocialAccount = baker.make(SocialAccount, uid=self.uid, provider="fxa") + sa.user.is_active = False + sa.user.save() + now_time = int(datetime.now().timestamp()) + # Note: FXA iat and exp are timestamps in *milliseconds* + exp_time = (now_time + 60 * 60) * 1000 + _setup_fxa_response(200, {"active": True, "sub": self.uid, "exp": exp_time}) + inactive_user_token = "inactive-user-123" + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"Bearer {inactive_user_token}") + + response = client.get("/api/v1/relayaddresses/") + assert response.status_code == 403 + expected_detail = ( + "Authenticated user does not have an active Relay account." + " Have they been deactivated?" + ) + assert response.json()["detail"] == expected_detail + @responses.activate def test_200_resp_from_fxa_for_user_returns_user_and_caches(self) -> None: sa: SocialAccount = baker.make(SocialAccount, uid=self.uid, provider="fxa") diff --git a/api/tests/emails_views_tests.py b/api/tests/emails_views_tests.py index da3256acf7..1bad574963 100644 --- a/api/tests/emails_views_tests.py +++ b/api/tests/emails_views_tests.py @@ -517,6 +517,24 @@ def test_post_relayaddress_flagged_error( assert get_glean_event(caplog) is None +def test_post_relayaddress_inactive_user_error( + free_user: User, free_api_client: APIClient, caplog: pytest.LogCaptureFixture +) -> None: + """An inactive user is unable to create a random mask.""" + free_user.is_active = False + free_user.save() + + response = free_api_client.post(reverse("relayaddress-list"), {}, format="json") + + assert response.status_code == 403 + ret_data = response.json() + assert ret_data == { + "detail": "Your account is not active.", + "error_code": "account_is_inactive", + } + assert get_glean_event(caplog) is None + + @pytest.mark.parametrize( "key,value", [ diff --git a/emails/models.py b/emails/models.py index 40b9d5fcf3..1f22d9efdf 100644 --- a/emails/models.py +++ b/emails/models.py @@ -664,6 +664,12 @@ class AccountIsPausedException(CannotMakeAddressException): status_code = 403 +class AccountIsInactiveException(CannotMakeAddressException): + default_code = "account_is_inactive" + default_detail = "Your account is not active." + status_code = 403 + + class RelayAddrFreeTierLimitException(CannotMakeAddressException): default_code = "free_tier_limit" default_detail_template = ( @@ -857,6 +863,9 @@ def metrics_id(self) -> str: def check_user_can_make_another_address(profile: Profile) -> None: + if not profile.user.is_active: + raise AccountIsInactiveException() + if profile.is_flagged: raise AccountIsPausedException() # MPP-3021: return early for premium users to avoid at_max_free_aliases DB query @@ -910,6 +919,9 @@ def check_user_can_make_domain_address(user_profile: Profile) -> None: if not user_profile.subdomain: raise DomainAddrNeedSubdomainException() + if not user_profile.user.is_active: + raise AccountIsInactiveException() + if user_profile.is_flagged: raise AccountIsPausedException() diff --git a/emails/tests/views_tests.py b/emails/tests/views_tests.py index 9529a4493f..40c9f5780e 100644 --- a/emails/tests/views_tests.py +++ b/emails/tests/views_tests.py @@ -1424,6 +1424,17 @@ def test_user_bounce_hard_paused_email_in_s3_deleted(self) -> None: self.assert_log_incoming_email_dropped(caplog, "hard_bounce_pause") mm.assert_incr_once("fx.private.relay.email_suppressed_for_hard_bounce") + def test_user_deactivated_email_in_s3_deleted(self) -> None: + self.profile.user.is_active = False + self.profile.user.save() + + with self.assertLogs(INFO_LOG) as caplog: + response = _sns_notification(EMAIL_SNS_BODIES["s3_stored"]) + self.mock_remove_message_from_s3.assert_called_once_with(self.bucket, self.key) + assert response.status_code == 200 + assert response.content == b"Account is deactivated." + self.assert_log_incoming_email_dropped(caplog, "inactive") + @patch("emails.views._reply_allowed") @patch("emails.views._get_reply_record_from_lookup_key") def test_reply_not_allowed_email_in_s3_deleted( diff --git a/emails/views.py b/emails/views.py index c8672ac50b..0784533038 100644 --- a/emails/views.py +++ b/emails/views.py @@ -512,6 +512,7 @@ def _sns_message(message_json: AWS_SNSMessageJSON) -> HttpResponse: "hard_bounce_pause", # The user recently had a hard bounce "soft_bounce_pause", # The user recently has a soft bounce "abuse_flag", # The user exceeded an abuse limit, like mails forwarded + "inactive", # The user account is deactivated "reply_requires_premium", # The email is a reply from a free user "content_missing", # Could not load the email from storage "error_from_header", # Error generating the From: header, retryable @@ -678,6 +679,10 @@ def _handle_received(message_json: AWS_SNSMessageJSON) -> HttpResponse: log_email_dropped(reason="abuse_flag", mask=address) return HttpResponse("Address is temporarily disabled.") + if not user_profile.user.is_active: + log_email_dropped(reason="inactive", mask=address) + return HttpResponse("Account is deactivated.") + # if address is set to block, early return if not address.enabled: incr_if_enabled("email_for_disabled_address", 1) diff --git a/privaterelay/pending_locales/en/pending.ftl b/privaterelay/pending_locales/en/pending.ftl index 5f35a2cc62..2f5e117871 100644 --- a/privaterelay/pending_locales/en/pending.ftl +++ b/privaterelay/pending_locales/en/pending.ftl @@ -3,3 +3,4 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. # This is the Django equivalent of frontend/pendingTranslations.ftl +api-error-account-is-inactive = Your account is not active. From 67ee589f2f06ea8ac092ebfac0f0d687630fdac3 Mon Sep 17 00:00:00 2001 From: groovecoder Date: Mon, 20 May 2024 10:11:29 -0500 Subject: [PATCH 2/8] for MPP-3817: new deactivate_user command --- .../commands/deactivate_user_by_token.py | 19 ------- .../management/commands/deactivate_user.py | 56 +++++++++++++++++++ 2 files changed, 56 insertions(+), 19 deletions(-) delete mode 100644 emails/management/commands/deactivate_user_by_token.py create mode 100644 privaterelay/management/commands/deactivate_user.py diff --git a/emails/management/commands/deactivate_user_by_token.py b/emails/management/commands/deactivate_user_by_token.py deleted file mode 100644 index 04df05b9f6..0000000000 --- a/emails/management/commands/deactivate_user_by_token.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.core.management.base import BaseCommand - -from ...models import Profile - - -class Command(BaseCommand): - help = "Removes an API token to effectively block access." - - def add_arguments(self, parser): - parser.add_argument("api_token", nargs=1) - - def handle(self, *args, **options): - try: - profile = Profile.objects.get(api_token=options["api_token"][0]) - profile.user.is_active = False - profile.user.save() - self.stdout.write("SUCCESS: deactivated user.") - except Profile.DoesNotExist: - self.stdout.write("ERROR: Could not find user with that token.") diff --git a/privaterelay/management/commands/deactivate_user.py b/privaterelay/management/commands/deactivate_user.py new file mode 100644 index 0000000000..9b5fea30b9 --- /dev/null +++ b/privaterelay/management/commands/deactivate_user.py @@ -0,0 +1,56 @@ +from argparse import ArgumentParser +from typing import Any + +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand + +from allauth.socialaccount.models import SocialAccount + +from emails.models import Profile + + +class Command(BaseCommand): + help = "Deactivates a user to effectively block all usage of Relay." + + def add_arguments(self, parser: ArgumentParser) -> None: + parser.add_argument("--key", type=str, help="User API key") + parser.add_argument("--email", type=str, help="User email address") + parser.add_argument("--uid", type=str, help="User FXA UID") + + def handle(self, *args: Any, **options: Any) -> str: + api_key: str | None = options.get("key") + email: str | None = options.get("email") + uid: str | None = options.get("uid") + + user: User + + if api_key: + try: + profile = Profile.objects.get(api_token=api_key) + user = profile.user + except Profile.DoesNotExist: + msg = "ERROR: Could not find user with that API key." + self.stderr.write(msg) + return msg + + if email: + try: + user = User.objects.get(email=email) + except User.DoesNotExist: + msg = "ERROR: Could not find user with that email address." + self.stderr.write(msg) + return msg + + if uid: + try: + user = SocialAccount.objects.get(uid=uid).user + except SocialAccount.DoesNotExist: + msg = "ERROR: Could not find user with that FXA UID." + self.stderr.write(msg) + return msg + + user.is_active = False + user.save() + msg = "SUCCESS: deactivated user." + self.stdout.write(msg) + return msg From 13f9c5b5a12ff0c62f17f0e05193f5461179e24b Mon Sep 17 00:00:00 2001 From: groovecoder Date: Mon, 20 May 2024 11:27:47 -0500 Subject: [PATCH 3/8] for MPP-3817: do not forward sms or calls for deactivated users --- api/tests/phones_views_tests.py | 49 +++++++++++++++++++++++++++++++++ api/views/phones.py | 11 ++++++++ 2 files changed, 60 insertions(+) diff --git a/api/tests/phones_views_tests.py b/api/tests/phones_views_tests.py index bdfa36d2ef..226185d7f2 100644 --- a/api/tests/phones_views_tests.py +++ b/api/tests/phones_views_tests.py @@ -847,6 +847,28 @@ def test_inbound_sms_valid_twilio_signature_unknown_number( assert "Could Not Find Relay Number." in response.data[0].title() +def test_inbound_sms_valid_twilio_signature_good_data_deactivated_user( + phone_user, mocked_twilio_client +): + phone_user.is_active = False + phone_user.save() + _make_real_phone(phone_user, verified=True) + relay_number = _make_relay_number(phone_user) + pre_inbound_remaining_texts = relay_number.remaining_texts + mocked_twilio_client.reset_mock() + + client = APIClient() + path = "/api/v1/inbound_sms" + data = {"From": "+15556660000", "To": relay_number.number, "Body": "test body"} + response = client.post(path, data, HTTP_X_TWILIO_SIGNATURE="valid") + + assert response.status_code == 200 + mocked_twilio_client.messages.create.assert_not_called() + relay_number.refresh_from_db() + assert relay_number.texts_forwarded == 0 + assert relay_number.remaining_texts == pre_inbound_remaining_texts + + def test_inbound_sms_valid_twilio_signature_good_data(phone_user, mocked_twilio_client): real_phone = _make_real_phone(phone_user, verified=True) relay_number = _make_relay_number(phone_user) @@ -1854,6 +1876,33 @@ def test_inbound_call_valid_twilio_signature_unknown_number( assert "Could Not Find Relay Number." in response.data[0].title() +def test_inbound_call_valid_twilio_signature_good_data_deactivated_user( + phone_user, mocked_twilio_client +): + _make_real_phone(phone_user, verified=True) + relay_number = _make_relay_number(phone_user, enabled=True) + phone_user.is_active = False + phone_user.save() + pre_call_calls_forwarded = relay_number.calls_forwarded + caller_number = "+15556660000" + mocked_twilio_client.reset_mock() + + client = APIClient() + path = "/api/v1/inbound_call" + data = {"Caller": caller_number, "Called": relay_number.number} + response = client.post(path, data, HTTP_X_TWILIO_SIGNATURE="valid") + + assert response.status_code == 200 + decoded_content = response.content.decode() + assert "" in decoded_content + relay_number.refresh_from_db() + assert relay_number.calls_forwarded == pre_call_calls_forwarded + with pytest.raises(InboundContact.DoesNotExist): + InboundContact.objects.get( + relay_number=relay_number, inbound_number=caller_number + ) + + def test_inbound_call_valid_twilio_signature_good_data( phone_user, mocked_twilio_client ): diff --git a/api/views/phones.py b/api/views/phones.py index 9b5cb3376c..bb0856d8ed 100644 --- a/api/views/phones.py +++ b/api/views/phones.py @@ -693,6 +693,12 @@ def inbound_sms(request): raise exceptions.ValidationError("Request missing From, To, Or Body.") relay_number, real_phone = _get_phone_objects(inbound_to) + if not real_phone.user.is_active: + return response.Response( + status=200, + template_name="twiml_empty_response.xml", + ) + _check_remaining(relay_number, "texts") if inbound_from == real_phone.number: @@ -939,6 +945,11 @@ def inbound_call(request): raise exceptions.ValidationError("Call data missing Caller or Called.") relay_number, real_phone = _get_phone_objects(inbound_to) + if not real_phone.user.is_active: + return response.Response( + status=200, + template_name="twiml_empty_response.xml", + ) number_disabled = _check_disabled(relay_number, "calls") if number_disabled: From 40688c9e76d98d16858b4508e9bfa5012eaaf8b7 Mon Sep 17 00:00:00 2001 From: groovecoder Date: Tue, 21 May 2024 14:45:21 -0500 Subject: [PATCH 4/8] for MPP-3817: add user.is_active check to has_premium() --- emails/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/emails/models.py b/emails/models.py index 1f22d9efdf..8e95fcc687 100644 --- a/emails/models.py +++ b/emails/models.py @@ -320,6 +320,9 @@ def custom_domain(self) -> str: @property def has_premium(self) -> bool: + if not self.user.is_active: + return False + # FIXME: as we don't have all the tiers defined we are over-defining # this to mark the user as a premium user as well if not self.fxa: From 581bb31e9ca19e03ac128ea3832e665f1d8d2bf4 Mon Sep 17 00:00:00 2001 From: groovecoder Date: Tue, 21 May 2024 14:46:06 -0500 Subject: [PATCH 5/8] for MPP-3817: add user.is_active check to _reply_allowed() --- emails/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/emails/views.py b/emails/views.py index 0784533038..e7117f81d7 100644 --- a/emails/views.py +++ b/emails/views.py @@ -512,7 +512,7 @@ def _sns_message(message_json: AWS_SNSMessageJSON) -> HttpResponse: "hard_bounce_pause", # The user recently had a hard bounce "soft_bounce_pause", # The user recently has a soft bounce "abuse_flag", # The user exceeded an abuse limit, like mails forwarded - "inactive", # The user account is deactivated + "user_deactivated", # The user account is deactivated "reply_requires_premium", # The email is a reply from a free user "content_missing", # Could not load the email from storage "error_from_header", # Error generating the From: header, retryable @@ -680,7 +680,7 @@ def _handle_received(message_json: AWS_SNSMessageJSON) -> HttpResponse: return HttpResponse("Address is temporarily disabled.") if not user_profile.user.is_active: - log_email_dropped(reason="inactive", mask=address) + log_email_dropped(reason="user_deactivated", mask=address) return HttpResponse("Account is deactivated.") # if address is set to block, early return @@ -1223,6 +1223,9 @@ def _reply_allowed( ): # This is a Relay user replying to an external sender; + if not reply_record.profile.user.is_active: + return False + if reply_record.profile.is_flagged: return False From 19370e5736eac2c41fabc8ae7549334067b747b9 Mon Sep 17 00:00:00 2001 From: groovecoder Date: Tue, 21 May 2024 14:47:32 -0500 Subject: [PATCH 6/8] for MPP-3817: add test for deactivate_user command --- .../management/commands/deactivate_user.py | 14 ++- .../tests/mgmt_deactivate_user_tests.py | 91 +++++++++++++++++++ 2 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 privaterelay/tests/mgmt_deactivate_user_tests.py diff --git a/privaterelay/management/commands/deactivate_user.py b/privaterelay/management/commands/deactivate_user.py index 9b5fea30b9..9334dcb7fc 100644 --- a/privaterelay/management/commands/deactivate_user.py +++ b/privaterelay/management/commands/deactivate_user.py @@ -28,6 +28,9 @@ def handle(self, *args: Any, **options: Any) -> str: try: profile = Profile.objects.get(api_token=api_key) user = profile.user + user.is_active = False + user.save() + msg = f"SUCCESS: deactivated user with api_token: {api_key}" except Profile.DoesNotExist: msg = "ERROR: Could not find user with that API key." self.stderr.write(msg) @@ -36,6 +39,9 @@ def handle(self, *args: Any, **options: Any) -> str: if email: try: user = User.objects.get(email=email) + user.is_active = False + user.save() + msg = f"SUCCESS: deactivated user with email: {email}" except User.DoesNotExist: msg = "ERROR: Could not find user with that email address." self.stderr.write(msg) @@ -44,13 +50,11 @@ def handle(self, *args: Any, **options: Any) -> str: if uid: try: user = SocialAccount.objects.get(uid=uid).user + user.is_active = False + user.save() + msg = f"SUCCESS: deactivated user with FXA UID: {uid}" except SocialAccount.DoesNotExist: msg = "ERROR: Could not find user with that FXA UID." self.stderr.write(msg) return msg - - user.is_active = False - user.save() - msg = "SUCCESS: deactivated user." - self.stdout.write(msg) return msg diff --git a/privaterelay/tests/mgmt_deactivate_user_tests.py b/privaterelay/tests/mgmt_deactivate_user_tests.py new file mode 100644 index 0000000000..523ba7d48e --- /dev/null +++ b/privaterelay/tests/mgmt_deactivate_user_tests.py @@ -0,0 +1,91 @@ +import os +import random +import string +import uuid +from io import StringIO + +from django.core.management import call_command + +import pytest +from allauth.socialaccount.models import SocialAccount +from model_bakery import baker + +COMMAND_NAME = "deactivate_user" + + +@pytest.mark.django_db +def test_deactivate_by_api_key() -> None: + sa: SocialAccount = baker.make(SocialAccount, provider="fxa") + api_token = sa.user.profile.api_token + out = StringIO() + + call_command(COMMAND_NAME, key=f"{api_token}", stdout=out) + + output = out.getvalue() + assert f"SUCCESS: deactivated user with api_token: {api_token}\n" == output + sa.user.refresh_from_db() + assert sa.user.is_active is False + + +@pytest.mark.django_db +def test_deactivate_by_api_key_does_not_exist() -> None: + out = StringIO() + api_token = uuid.uuid4() + + call_command(COMMAND_NAME, key=f"{api_token}", stdout=out) + + output = out.getvalue() + assert "ERROR: Could not find user with that API key.\n" == output + + +@pytest.mark.django_db +def test_deactivate_by_email() -> None: + localpart = "".join(random.choice(string.ascii_lowercase) for i in range(9)) + email = f"{localpart}@test.com" + sa: SocialAccount = baker.make(SocialAccount, provider="fxa") + sa.user.email = email + sa.user.save() + out = StringIO() + + call_command(COMMAND_NAME, email=f"{email}", stdout=out) + + output = out.getvalue() + assert f"SUCCESS: deactivated user with email: {email}\n" == output + sa.user.refresh_from_db() + assert sa.user.is_active is False + + +@pytest.mark.django_db +def test_deactivate_by_email_does_not_exist() -> None: + out = StringIO() + localpart = "".join(random.choice(string.ascii_lowercase) for i in range(9)) + email = f"{localpart}@test.com" + + call_command(COMMAND_NAME, email=f"{email}", stdout=out) + + output = out.getvalue() + assert "ERROR: Could not find user with that email address.\n" == output + + +@pytest.mark.django_db +def test_deactivate_by_fxa_uid() -> None: + sa: SocialAccount = baker.make(SocialAccount, provider="fxa") + out = StringIO() + + call_command(COMMAND_NAME, uid=f"{sa.uid}", stdout=out) + + output = out.getvalue() + assert f"SUCCESS: deactivated user with FXA UID: {sa.uid}\n" == output + sa.user.refresh_from_db() + assert sa.user.is_active is False + + +@pytest.mark.django_db +def test_deactivate_by_fxa_uid_does_not_exist() -> None: + out = StringIO() + uid = os.urandom(16).hex() + + call_command(COMMAND_NAME, uid=f"{uid}", stdout=out) + + output = out.getvalue() + assert "ERROR: Could not find user with that FXA UID.\n" == output From fa31e8afc670c60e95772dd086330a2fdf84b9e9 Mon Sep 17 00:00:00 2001 From: groovecoder Date: Tue, 21 May 2024 15:05:16 -0500 Subject: [PATCH 7/8] MPP-3817: fix test assertion to user_deactivated --- emails/tests/views_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/emails/tests/views_tests.py b/emails/tests/views_tests.py index 40c9f5780e..4360d7eb68 100644 --- a/emails/tests/views_tests.py +++ b/emails/tests/views_tests.py @@ -1433,7 +1433,7 @@ def test_user_deactivated_email_in_s3_deleted(self) -> None: self.mock_remove_message_from_s3.assert_called_once_with(self.bucket, self.key) assert response.status_code == 200 assert response.content == b"Account is deactivated." - self.assert_log_incoming_email_dropped(caplog, "inactive") + self.assert_log_incoming_email_dropped(caplog, "user_deactivated") @patch("emails.views._reply_allowed") @patch("emails.views._get_reply_record_from_lookup_key") From f937381e51c9c42618d51a3ea64a256cae7a4736 Mon Sep 17 00:00:00 2001 From: groovecoder Date: Fri, 24 May 2024 11:17:38 -0500 Subject: [PATCH 8/8] for MPP-3817: redirect inactive users to new account_inactive page --- frontend/pendingTranslations.ftl | 2 ++ .../accounts/account_inactive.module.scss | 6 ++++++ .../pages/accounts/account_inactive.page.tsx | 18 ++++++++++++++++++ privaterelay/allauth.py | 5 ++++- 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/accounts/account_inactive.module.scss create mode 100644 frontend/src/pages/accounts/account_inactive.page.tsx diff --git a/frontend/pendingTranslations.ftl b/frontend/pendingTranslations.ftl index 4121243877..2f220679b4 100644 --- a/frontend/pendingTranslations.ftl +++ b/frontend/pendingTranslations.ftl @@ -124,3 +124,5 @@ upsell-banner-4-masks-us-cta = Upgrade to { -brand-name-relay-premium } -brand-name-mozilla-monitor = Mozilla Monitor moz-monitor = { -brand-name-mozilla-monitor } + +api-error-account-is-inactive = Your account is not active. diff --git a/frontend/src/pages/accounts/account_inactive.module.scss b/frontend/src/pages/accounts/account_inactive.module.scss new file mode 100644 index 0000000000..4346c56bed --- /dev/null +++ b/frontend/src/pages/accounts/account_inactive.module.scss @@ -0,0 +1,6 @@ +@import "~@mozilla-protocol/core/protocol/css/includes/lib"; + +.error { + background-color: $color-red-60; + color: $color-white; +} diff --git a/frontend/src/pages/accounts/account_inactive.page.tsx b/frontend/src/pages/accounts/account_inactive.page.tsx new file mode 100644 index 0000000000..40c003a1a6 --- /dev/null +++ b/frontend/src/pages/accounts/account_inactive.page.tsx @@ -0,0 +1,18 @@ +import type { NextPage } from "next"; + +import { Layout } from "../../components/layout/Layout"; +import { useL10n } from "../../hooks/l10n"; +import styles from "./account_inactive.module.scss"; + +const AccountInactive: NextPage = () => { + const l10n = useL10n(); + return ( + +
+ {l10n.getString("api-error-account-is-inactive")} +
+
+ ); +}; + +export default AccountInactive; diff --git a/privaterelay/allauth.py b/privaterelay/allauth.py index 0d72246bcc..2db0478cac 100644 --- a/privaterelay/allauth.py +++ b/privaterelay/allauth.py @@ -2,7 +2,7 @@ from urllib.parse import urlencode, urlparse from django.http import Http404 -from django.shortcuts import resolve_url +from django.shortcuts import redirect, resolve_url from django.urls import resolve from allauth.account.adapter import DefaultAccountAdapter @@ -54,3 +54,6 @@ def is_safe_url(self, url: str | None) -> bool: # The path is invalid logger.error("No matching URL for '%s'", url) return False + + def respond_user_inactive(self, request, user): + return redirect("/accounts/account_inactive/")