Added Super User Creator

Added creator
Corrected license in swagger
This commit is contained in:
Dispatcharr 2025-03-03 14:57:38 -06:00
parent 9feccc76cc
commit 814d6a6cda
6 changed files with 166 additions and 12 deletions

View file

@ -2,7 +2,7 @@ from django.urls import path, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from .api_views import ( from .api_views import (
AuthViewSet, UserViewSet, GroupViewSet, AuthViewSet, UserViewSet, GroupViewSet,
list_permissions list_permissions, initialize_superuser
) )
from rest_framework_simplejwt import views as jwt_views from rest_framework_simplejwt import views as jwt_views
@ -28,6 +28,9 @@ urlpatterns = [
path('auth/login/', auth_view, name='user-login'), path('auth/login/', auth_view, name='user-login'),
path('auth/logout/', logout_view, name='user-logout'), path('auth/logout/', logout_view, name='user-logout'),
# Superuser API
path('initialize-superuser/', initialize_superuser, name='initialize_superuser'),
# Permissions API # Permissions API
path('permissions/', list_permissions, name='list-permissions'), path('permissions/', list_permissions, name='list-permissions'),

View file

@ -1,14 +1,39 @@
from django.contrib.auth import authenticate, login, logout from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
from django.http import JsonResponse, HttpResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import viewsets from rest_framework import viewsets
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi from drf_yasg import openapi
import json
from .models import User from .models import User
from .serializers import UserSerializer, GroupSerializer, PermissionSerializer from .serializers import UserSerializer, GroupSerializer, PermissionSerializer
@csrf_exempt # In production, consider CSRF protection strategies or ensure this endpoint is only accessible when no superuser exists.
def initialize_superuser(request):
# If a superuser already exists, always indicate that
if User.objects.filter(is_superuser=True).exists():
return JsonResponse({"superuser_exists": True})
if request.method == "POST":
try:
data = json.loads(request.body)
username = data.get("username")
password = data.get("password")
email = data.get("email", "")
if not username or not password:
return JsonResponse({"error": "Username and password are required."}, status=400)
# Create the superuser
User.objects.create_superuser(username=username, password=password, email=email)
return JsonResponse({"superuser_exists": True})
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
# For GET requests, indicate no superuser exists
return JsonResponse({"superuser_exists": False})
# 🔹 1) Authentication APIs # 🔹 1) Authentication APIs
class AuthViewSet(viewsets.ViewSet): class AuthViewSet(viewsets.ViewSet):

View file

@ -8,4 +8,5 @@ urlpatterns = [
path('login/', auth_views.LoginView.as_view(template_name='login.html'), name='login'), path('login/', auth_views.LoginView.as_view(template_name='login.html'), name='login'),
# Logout view using Django's built-in authentication # Logout view using Django's built-in authentication
path('logout/', auth_views.LogoutView.as_view(next_page='accounts:login'), name='logout'), path('logout/', auth_views.LogoutView.as_view(next_page='accounts:login'), name='logout'),
# Onetime use superuser account creation
] ]

View file

@ -15,7 +15,7 @@ schema_view = get_schema_view(
description="API documentation for Dispatcharr", description="API documentation for Dispatcharr",
terms_of_service="https://www.google.com/policies/terms/", terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="contact@dispatcharr.local"), contact=openapi.Contact(email="contact@dispatcharr.local"),
license=openapi.License(name="Unlicense"), license=openapi.License(name="Creative Commons by-nc-sa"),
), ),
public=True, public=True,
permission_classes=(permissions.AllowAny,), permission_classes=(permissions.AllowAny,),

View file

@ -1,5 +1,6 @@
// frontend/src/App.js // frontend/src/App.js
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { import {
BrowserRouter as Router, BrowserRouter as Router,
Route, Route,
@ -29,17 +30,16 @@ import StreamProfiles from './pages/StreamProfiles';
import useAuthStore from './store/auth'; import useAuthStore from './store/auth';
import logo from './images/logo.png'; import logo from './images/logo.png';
import Alert from './components/Alert'; import Alert from './components/Alert';
// NEW: import the floating PiP component
import FloatingVideo from './components/FloatingVideo'; import FloatingVideo from './components/FloatingVideo';
import SuperuserForm from './components/forms/SuperuserForm';
const drawerWidth = 240; const drawerWidth = 240;
const miniDrawerWidth = 60; const miniDrawerWidth = 60;
const defaultRoute = '/channels'; const defaultRoute = '/channels';
const App = () => { const App = () => {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
const [needsSuperuser, setNeedsSuperuser] = useState(false);
const { const {
isAuthenticated, isAuthenticated,
setIsAuthenticated, setIsAuthenticated,
@ -52,6 +52,22 @@ const App = () => {
setOpen(!open); setOpen(!open);
}; };
// Check if a superuser exists on first load.
useEffect(() => {
async function checkSuperuser() {
try {
const res = await axios.get('/api/accounts/initialize-superuser/');
if (!res.data.superuser_exists) {
setNeedsSuperuser(true);
}
} catch (error) {
console.error('Error checking superuser status:', error);
}
}
checkSuperuser();
}, []);
// Authentication check.
useEffect(() => { useEffect(() => {
const checkAuth = async () => { const checkAuth = async () => {
const loggedIn = await initializeAuth(); const loggedIn = await initializeAuth();
@ -65,6 +81,11 @@ const App = () => {
checkAuth(); checkAuth();
}, [initializeAuth, initData, setIsAuthenticated, logout]); }, [initializeAuth, initData, setIsAuthenticated, logout]);
// If no superuser exists, show the initialization form.
if (needsSuperuser) {
return <SuperuserForm onSuccess={() => setNeedsSuperuser(false)} />;
}
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
@ -99,12 +120,9 @@ const App = () => {
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
</List> </List>
<Divider /> <Divider />
<Sidebar open /> <Sidebar open />
</Drawer> </Drawer>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@ -149,11 +167,7 @@ const App = () => {
</Box> </Box>
</Box> </Box>
</Router> </Router>
{/* Global Snackbar for system-wide messages */}
<Alert /> <Alert />
{/* Always-available floating video; remains visible across page changes */}
<FloatingVideo /> <FloatingVideo />
</ThemeProvider> </ThemeProvider>
); );

View file

@ -0,0 +1,111 @@
// frontend/src/components/forms/SuperuserForm.js
import React, { useState } from 'react';
import axios from 'axios';
import { Box, Paper, Typography, Grid as Grid2, TextField, Button } from '@mui/material';
function SuperuserForm({ onSuccess }) {
const [formData, setFormData] = useState({
username: '',
password: '',
email: '',
});
const [error, setError] = useState('');
const handleChange = (e) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const res = await axios.post('/api/accounts/initialize-superuser/', {
username: formData.username,
password: formData.password,
email: formData.email,
});
if (res.data.superuser_exists) {
onSuccess();
}
} catch (err) {
let msg = 'Failed to create superuser.';
if (err.response && err.response.data && err.response.data.error) {
msg += ` ${err.response.data.error}`;
}
setError(msg);
}
};
return (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
backgroundColor: '#f5f5f5',
}}
>
<Paper elevation={3} sx={{ padding: 3, width: '100%', maxWidth: 400 }}>
<Typography variant="h5" align="center" gutterBottom>
Create your Super User Account
</Typography>
{error && (
<Typography variant="body2" color="error" sx={{ mb: 2 }}>
{error}
</Typography>
)}
<form onSubmit={handleSubmit}>
<Grid2 container spacing={2} justifyContent="center" direction="column">
<Grid2 item xs={12}>
<TextField
label="Username"
variant="standard"
fullWidth
name="username"
value={formData.username}
onChange={handleChange}
required
size="small"
/>
</Grid2>
<Grid2 item xs={12}>
<TextField
label="Password"
variant="standard"
type="password"
fullWidth
name="password"
value={formData.password}
onChange={handleChange}
required
size="small"
/>
</Grid2>
<Grid2 item xs={12}>
<TextField
label="Email (optional)"
variant="standard"
type="email"
fullWidth
name="email"
value={formData.email}
onChange={handleChange}
size="small"
/>
</Grid2>
<Grid2 item xs={12}>
<Button type="submit" variant="contained" color="primary" fullWidth>
Create Superuser
</Button>
</Grid2>
</Grid2>
</form>
</Paper>
</Box>
);
}
export default SuperuserForm;