mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
Added Super User Creator
Added creator Corrected license in swagger
This commit is contained in:
parent
9feccc76cc
commit
814d6a6cda
6 changed files with 166 additions and 12 deletions
|
|
@ -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'),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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,),
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
111
frontend/src/components/forms/SuperuserForm.js
Normal file
111
frontend/src/components/forms/SuperuserForm.js
Normal 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;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue