mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
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)
198 lines
6.5 KiB
JavaScript
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;
|