mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-17 16:50:53 +00:00
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:
parent
db318e4981
commit
f453380f5c
4 changed files with 2 additions and 273 deletions
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -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 }}>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue