+
+ {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/")
diff --git a/privaterelay/management/commands/deactivate_user.py b/privaterelay/management/commands/deactivate_user.py
new file mode 100644
index 0000000000..9334dcb7fc
--- /dev/null
+++ b/privaterelay/management/commands/deactivate_user.py
@@ -0,0 +1,60 @@
+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
+ 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)
+ return msg
+
+ 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)
+ return msg
+
+ 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
+ return msg
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.
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