Dispatcharr/frontend/src/App.jsx
None db318e4981 Fix #954: Superuser detection; Feature #1004: User account disable/enable
Bug #954: The initialize_superuser endpoint only checked is_superuser=True, missing admin accounts created via the API (user_level=10). This caused users to intermittently see the "Create Super User" page instead of login.

Feature #1004: Admins can now disable/enable user accounts. Disabled users are blocked from JWT login, token refresh, XC API access, and all
authenticated endpoints. The last active admin cannot be disabled.

Files changed:

Backend:
- apps/accounts/api_views.py — Superuser check uses user_level>=10; token refresh blocks disabled users
- apps/accounts/models.py — CustomUserManager ensures create_superuser() always sets user_level=10
- apps/accounts/serializers.py — Last-admin protection guard; respect is_active on user creation
- apps/accounts/tests.py — new tests covering superuser detection, token
 refresh blocking, last-admin protection, disabled user login/access
- apps/backups/api_views.py — Switched from DRF's IsAdminUser (checks is_staff) to app's IsAdmin (checks user_level) on all 8 endpoints
- apps/backups/tests.py — new tests verifying user_level-based admin permission on backup endpoints
- apps/output/views.py — is_active check on xc_get_user(), xc_movie_stream(), xc_series_stream()
- apps/proxy/ts_proxy/views.py — is_active check on stream_xc()
- core/api_views.py — Admin notification filter uses user_level>=10
- core/developer_notifications.py — Admin checks use user_level>=10

Frontend:
- frontend/src/App.jsx — Null-safe superuser existence check
- frontend/src/components/forms/User.jsx — Account Enabled switch with
  self-disable prevention and tooltip
- frontend/src/components/tables/UsersTable.jsx — Status column (Active/Disabled badge)
2026-02-22 18:05:02 -06:00

198 lines
6.5 KiB
JavaScript

import React, { useEffect, useState, useRef } from 'react';
import {
BrowserRouter as Router,
Route,
Routes,
Navigate,
} from 'react-router-dom';
import Sidebar from './components/Sidebar';
import Login from './pages/Login';
import Channels from './pages/Channels';
import ContentSources from './pages/ContentSources';
import Guide from './pages/Guide';
import Stats from './pages/Stats';
import DVR from './pages/DVR';
import Settings from './pages/Settings';
import PluginsPage from './pages/Plugins';
import ConnectPage from './pages/Connect';
import ConnectLogsPage from './pages/ConnectLogs';
import Users from './pages/Users';
import LogosPage from './pages/Logos';
import VODsPage from './pages/VODs';
import useAuthStore from './store/auth';
import FloatingVideo from './components/FloatingVideo';
import { WebsocketProvider } from './WebSocket';
import { Box, AppShell, MantineProvider } from '@mantine/core';
import '@mantine/core/styles.css'; // Ensure Mantine global styles load
import '@mantine/notifications/styles.css';
import '@mantine/dropzone/styles.css';
import '@mantine/dates/styles.css';
import './index.css';
import mantineTheme from './mantineTheme';
import API from './api';
import { Notifications } from '@mantine/notifications';
import M3URefreshNotification from './components/M3URefreshNotification';
import 'allotment/dist/style.css';
const drawerWidth = 240;
const miniDrawerWidth = 60;
const defaultRoute = '/channels';
const App = () => {
const [open, setOpen] = useState(true);
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const isInitialized = useAuthStore((s) => s.isInitialized);
const setIsAuthenticated = useAuthStore((s) => s.setIsAuthenticated);
const logout = useAuthStore((s) => s.logout);
const initData = useAuthStore((s) => s.initData);
const initializeAuth = useAuthStore((s) => s.initializeAuth);
const setSuperuserExists = useAuthStore((s) => s.setSuperuserExists);
const authCheckStarted = useRef(false);
const superuserCheckStarted = useRef(false);
const toggleDrawer = () => {
setOpen(!open);
};
// Check if a superuser exists on first load.
useEffect(() => {
if (superuserCheckStarted.current) return;
superuserCheckStarted.current = true;
async function checkSuperuser() {
try {
const response = await API.fetchSuperUser();
if (response && response.superuser_exists === false) {
setSuperuserExists(false);
}
} catch (error) {
console.error('Error checking superuser status:', error);
// If authentication error, redirect to login
if (error.status === 401) {
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
}
}
}
checkSuperuser();
}, [setSuperuserExists]);
// Authentication check
useEffect(() => {
if (authCheckStarted.current) return;
authCheckStarted.current = true;
const checkAuth = async () => {
try {
const loggedIn = await initializeAuth();
if (loggedIn) {
await initData();
// Logos are now loaded at the end of initData, no need for background loading
} else {
await logout();
}
} catch (error) {
console.error('Auth check failed:', error);
await logout();
}
};
checkAuth();
}, [initializeAuth, initData, logout]);
return (
<MantineProvider
defaultColorScheme="dark"
theme={mantineTheme}
withGlobalStyles
withNormalizeCSS
>
<WebsocketProvider>
<Router>
<AppShell
header={{
height: 0,
}}
navbar={{
width:
isAuthenticated && isInitialized
? open
? drawerWidth
: miniDrawerWidth
: 0,
}}
>
{isAuthenticated && isInitialized && (
<Sidebar
drawerWidth={drawerWidth}
miniDrawerWidth={miniDrawerWidth}
collapsed={!open}
toggleDrawer={toggleDrawer}
/>
)}
<AppShell.Main>
<Box
style={{
display: 'flex',
flexDirection: 'column',
// transition: 'margin-left 0.3s',
backgroundColor: '#18181b',
height: '100vh',
color: 'white',
}}
>
<Box sx={{ p: 2, flex: 1, overflow: 'auto' }}>
<Routes>
{isAuthenticated && isInitialized ? (
<>
<Route path="/channels" element={<Channels />} />
<Route path="/sources" element={<ContentSources />} />
<Route path="/guide" element={<Guide />} />
<Route path="/dvr" element={<DVR />} />
<Route path="/stats" element={<Stats />} />
<Route path="/plugins" element={<PluginsPage />} />
<Route path="/connect" element={<ConnectPage />} />
<Route
path="/connect/logs"
element={<ConnectLogsPage />}
/>
<Route path="/users" element={<Users />} />
<Route path="/settings" element={<Settings />} />
<Route path="/logos" element={<LogosPage />} />
<Route path="/vods" element={<VODsPage />} />
</>
) : (
<Route path="/login" element={<Login needsSuperuser />} />
)}
<Route
path="*"
element={
<Navigate
to={
isAuthenticated && isInitialized
? defaultRoute
: '/login'
}
replace
/>
}
/>
</Routes>
</Box>
</Box>
</AppShell.Main>
</AppShell>
<M3URefreshNotification />
<Notifications containerWidth={350} />
</Router>
</WebsocketProvider>
<FloatingVideo />
</MantineProvider>
);
};
export default App;