Remove user account disable/enable UI (keep backend guards)

Removed the frontend UI exposure
while keeping backend guards as defensive measures.

Removed:
- frontend/src/components/forms/User.jsx — Account Enabled switch
- frontend/src/components/tables/UsersTable.jsx — Status column
- apps/accounts/api_views.py — is_active check in TokenRefreshView
- apps/accounts/tests.py — related tests
This commit is contained in:
None 2026-02-22 20:02:08 -06:00
parent db318e4981
commit f453380f5c
4 changed files with 2 additions and 273 deletions

View file

@ -113,24 +113,6 @@ class TokenRefreshView(TokenRefreshView):
)
return Response({"error": "Unauthorized"}, status=status.HTTP_403_FORBIDDEN)
# Check if user account is still active before issuing new access token
raw_token = request.data.get("refresh")
if raw_token:
try:
from rest_framework_simplejwt.tokens import RefreshToken as RefreshTokenClass
token = RefreshTokenClass(raw_token)
user_id = token.payload.get("user_id")
if user_id:
user = User.objects.filter(id=user_id).first()
if user and not user.is_active:
logger.info(f"Token refresh blocked for disabled user: user_id={user_id}")
return Response(
{"error": "Account is disabled"},
status=status.HTTP_403_FORBIDDEN
)
except Exception:
pass # Let parent handle invalid tokens
return super().post(request, *args, **kwargs)

View file

@ -1,7 +1,6 @@
from django.test import TestCase, RequestFactory
from django.test import TestCase
from django.contrib.auth import get_user_model
from rest_framework.test import APIClient
from rest_framework import status
User = get_user_model()
@ -70,214 +69,4 @@ class InitializeSuperuserTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertTrue(response.json()["superuser_exists"])
# Should NOT have created a new user
self.assertFalse(User.objects.filter(username="newadmin").exists())
class TokenRefreshDisabledUserTests(TestCase):
"""Tests for blocking token refresh on disabled users"""
def setUp(self):
self.client = APIClient()
self.token_url = "/api/accounts/token/"
self.refresh_url = "/api/accounts/token/refresh/"
def test_refresh_works_for_active_user(self):
"""Active user should be able to refresh their token"""
user = User.objects.create_user(username="active", password="testpass123")
user.user_level = 1
user.is_active = True
user.save()
# Get tokens
login_response = self.client.post(
self.token_url,
{"username": "active", "password": "testpass123"},
format="json",
)
self.assertEqual(login_response.status_code, 200)
refresh_token = login_response.data["refresh"]
# Refresh should succeed
refresh_response = self.client.post(
self.refresh_url,
{"refresh": refresh_token},
format="json",
)
self.assertEqual(refresh_response.status_code, 200)
self.assertIn("access", refresh_response.data)
def test_refresh_blocked_for_disabled_user(self):
"""Disabled user should not be able to refresh their token"""
user = User.objects.create_user(username="disabled", password="testpass123")
user.user_level = 1
user.is_active = True
user.save()
# Get tokens while user is active
login_response = self.client.post(
self.token_url,
{"username": "disabled", "password": "testpass123"},
format="json",
)
self.assertEqual(login_response.status_code, 200)
refresh_token = login_response.data["refresh"]
# Disable the user
user.is_active = False
user.save()
# Refresh should be blocked
refresh_response = self.client.post(
self.refresh_url,
{"refresh": refresh_token},
format="json",
)
self.assertEqual(refresh_response.status_code, 403)
class LastAdminProtectionTests(TestCase):
"""Tests for preventing disabling the last active admin"""
def setUp(self):
self.admin = User.objects.create_user(username="admin1", password="testpass123")
self.admin.user_level = 10
self.admin.save()
self.client = APIClient()
self.client.force_authenticate(user=self.admin)
def test_cannot_disable_last_admin(self):
"""Should reject disabling the only active admin"""
response = self.client.patch(
f"/api/accounts/users/{self.admin.id}/",
{"is_active": False},
format="json",
)
self.assertEqual(response.status_code, 400)
self.assertIn("is_active", response.data)
# Verify admin is still active
self.admin.refresh_from_db()
self.assertTrue(self.admin.is_active)
def test_can_disable_admin_when_another_exists(self):
"""Should allow disabling an admin when another active admin exists"""
admin2 = User.objects.create_user(username="admin2", password="testpass123")
admin2.user_level = 10
admin2.save()
response = self.client.patch(
f"/api/accounts/users/{admin2.id}/",
{"is_active": False},
format="json",
)
self.assertEqual(response.status_code, 200)
admin2.refresh_from_db()
self.assertFalse(admin2.is_active)
def test_can_disable_non_admin_user(self):
"""Should always allow disabling non-admin users"""
regular = User.objects.create_user(username="regular", password="testpass123")
regular.user_level = 1
regular.save()
response = self.client.patch(
f"/api/accounts/users/{regular.id}/",
{"is_active": False},
format="json",
)
self.assertEqual(response.status_code, 200)
regular.refresh_from_db()
self.assertFalse(regular.is_active)
def test_can_reenable_disabled_user(self):
"""Should allow re-enabling a disabled user"""
regular = User.objects.create_user(username="disabled", password="testpass123")
regular.user_level = 1
regular.is_active = False
regular.save()
response = self.client.patch(
f"/api/accounts/users/{regular.id}/",
{"is_active": True},
format="json",
)
self.assertEqual(response.status_code, 200)
regular.refresh_from_db()
self.assertTrue(regular.is_active)
class DisabledUserLoginTests(TestCase):
"""Tests that disabled users cannot log in"""
def setUp(self):
self.client = APIClient()
self.token_url = "/api/accounts/token/"
def test_disabled_user_cannot_login(self):
"""Disabled user should get rejected at login"""
user = User.objects.create_user(username="disabled", password="testpass123")
user.is_active = False
user.save()
response = self.client.post(
self.token_url,
{"username": "disabled", "password": "testpass123"},
format="json",
)
self.assertEqual(response.status_code, 401)
def test_active_user_can_login(self):
"""Active user should be able to log in"""
user = User.objects.create_user(username="active", password="testpass123")
user.is_active = True
user.save()
response = self.client.post(
self.token_url,
{"username": "active", "password": "testpass123"},
format="json",
)
self.assertEqual(response.status_code, 200)
self.assertIn("access", response.data)
class DisabledUserAccessTokenTests(TestCase):
"""Tests that a disabled user's existing access token is rejected on authenticated endpoints"""
def setUp(self):
self.client = APIClient()
self.token_url = "/api/accounts/token/"
self.users_url = "/api/accounts/users/"
def test_existing_token_rejected_after_disable(self):
"""Access token obtained while active should be rejected after user is disabled"""
user = User.objects.create_user(username="willdisable", password="testpass123")
user.user_level = 10 # Admin so they can access the users endpoint
user.is_active = True
user.save()
# Get tokens while user is active
login_response = self.client.post(
self.token_url,
{"username": "willdisable", "password": "testpass123"},
format="json",
)
self.assertEqual(login_response.status_code, 200)
access_token = login_response.data["access"]
# Verify token works while active
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {access_token}")
response = self.client.get(self.users_url)
self.assertEqual(response.status_code, 200)
# Disable the user
user.is_active = False
user.save()
# Same token should now be rejected
response = self.client.get(self.users_url)
self.assertIn(response.status_code, [401, 403])
self.assertFalse(User.objects.filter(username="newadmin").exists())

View file

@ -42,7 +42,6 @@ const User = ({ user = null, isOpen, onClose }) => {
xc_password: '',
channel_profiles: [],
hide_adult_content: false,
is_active: true,
},
validate: (values) => ({
@ -135,7 +134,6 @@ const User = ({ user = null, isOpen, onClose }) => {
: ['0'],
xc_password: customProps.xc_password || '',
hide_adult_content: customProps.hide_adult_content || false,
is_active: user.is_active !== false,
});
if (customProps.xc_password) {
@ -203,30 +201,6 @@ const User = ({ user = null, isOpen, onClose }) => {
/>
)}
{isAdmin && (
<Box>
<Tooltip
label={isEditingSelf
? "You cannot disable your own account"
: "Disabled accounts cannot log in or access any resources"
}
position="top"
withArrow
>
<div style={isEditingSelf ? { cursor: 'not-allowed' } : undefined}>
<Switch
label="Account Enabled"
{...form.getInputProps('is_active', {
type: 'checkbox',
})}
key={form.key('is_active')}
disabled={isEditingSelf}
styles={isEditingSelf ? { track: { pointerEvents: 'none' } } : undefined}
/>
</div>
</Tooltip>
</Box>
)}
</Stack>
<Stack gap="xs" style={{ flex: 1 }}>

View file

@ -8,7 +8,6 @@ import useWarningsStore from '../../store/warnings';
import { SquarePlus, SquareMinus, SquarePen, Eye, EyeOff } from 'lucide-react';
import {
ActionIcon,
Badge,
Box,
Text,
Paper,
@ -148,20 +147,6 @@ const UsersTable = () => {
<Text size="sm">{USER_LEVEL_LABELS[getValue()]}</Text>
),
},
{
header: 'Status',
accessorKey: 'is_active',
size: 90,
cell: ({ getValue }) => (
<Badge
size="sm"
color={getValue() !== false ? 'green' : 'red'}
variant="light"
>
{getValue() !== false ? 'Active' : 'Disabled'}
</Badge>
),
},
{
header: 'Username',
accessorKey: 'username',
@ -331,7 +316,6 @@ const UsersTable = () => {
name: renderHeaderCell,
email: renderHeaderCell,
user_level: renderHeaderCell,
is_active: renderHeaderCell,
last_login: renderHeaderCell,
date_joined: renderHeaderCell,
custom_properties: renderHeaderCell,