mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
user custom properties, xc has its own password, properly checking xc permissions for streaming
This commit is contained in:
parent
e3553b04ad
commit
e95c0859ab
6 changed files with 127 additions and 52 deletions
|
|
@ -34,5 +34,10 @@ class Migration(migrations.Migration):
|
|||
name="user_level",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="custom_properties",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.RunPython(set_user_level_to_10),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class User(AbstractUser):
|
|||
related_name="users",
|
||||
)
|
||||
user_level = models.IntegerField(default=UserLevel.STREAMER)
|
||||
custom_properties = models.TextField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
"user_level",
|
||||
"password",
|
||||
"channel_profiles",
|
||||
"custom_properties",
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ from rest_framework.response import Response
|
|||
from django.urls import reverse
|
||||
from apps.channels.models import Channel, ChannelProfile, ChannelGroup
|
||||
from apps.epg.models import ProgramData
|
||||
from apps.accounts.models import User
|
||||
from django.utils import timezone
|
||||
from django.shortcuts import get_object_or_404
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
import html # Add this import for XML escaping
|
||||
from django.contrib.auth import authenticate
|
||||
from tzlocal import get_localzone
|
||||
import time
|
||||
import json
|
||||
|
|
@ -413,18 +414,31 @@ def generate_epg(request, profile_name=None, user=None):
|
|||
return response
|
||||
|
||||
|
||||
def xc_player_api(request):
|
||||
action = request.GET.get("action")
|
||||
def xc_get_user(request):
|
||||
username = request.GET.get("username")
|
||||
password = request.GET.get("password")
|
||||
|
||||
if not username or not password:
|
||||
raise Http404()
|
||||
|
||||
user = authenticate(
|
||||
username=request.GET.get("username"), password=request.GET.get("password")
|
||||
user = get_object_or_404(User, username=username)
|
||||
custom_properties = (
|
||||
json.loads(user.custom_properties) if user.custom_properties else {}
|
||||
)
|
||||
|
||||
if "xc_password" not in custom_properties:
|
||||
raise Http404()
|
||||
|
||||
if custom_properties["xc_password"] != password:
|
||||
raise Http404()
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def xc_player_api(request):
|
||||
action = request.GET.get("action")
|
||||
user = xc_get_user(request)
|
||||
|
||||
if user is None:
|
||||
raise Http404()
|
||||
|
||||
|
|
@ -439,8 +453,8 @@ def xc_player_api(request):
|
|||
return JsonResponse(
|
||||
{
|
||||
"user_info": {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"username": request.GET.get("username"),
|
||||
"password": request.GET.get("password"),
|
||||
"message": "",
|
||||
"auth": 1,
|
||||
"status": "Active",
|
||||
|
|
@ -470,15 +484,7 @@ def xc_player_api(request):
|
|||
|
||||
def xc_get(request):
|
||||
action = request.GET.get("action")
|
||||
username = request.GET.get("username")
|
||||
password = request.GET.get("password")
|
||||
|
||||
if not username or not password:
|
||||
raise Http404()
|
||||
|
||||
user = authenticate(
|
||||
username=request.GET.get("username"), password=request.GET.get("password")
|
||||
)
|
||||
user = xc_get_user(request)
|
||||
|
||||
if user is None:
|
||||
raise Http404()
|
||||
|
|
@ -488,15 +494,7 @@ def xc_get(request):
|
|||
|
||||
|
||||
def xc_xmltv(request):
|
||||
username = request.GET.get("username")
|
||||
password = request.GET.get("password")
|
||||
|
||||
if not username or not password:
|
||||
raise Http404()
|
||||
|
||||
user = authenticate(
|
||||
username=request.GET.get("username"), password=request.GET.get("password")
|
||||
)
|
||||
user = xc_get_user(request)
|
||||
|
||||
if user is None:
|
||||
raise Http404()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import re
|
|||
from django.http import StreamingHttpResponse, JsonResponse, HttpResponseRedirect
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.contrib.auth import authenticate
|
||||
from apps.proxy.config import TSConfig as Config
|
||||
from .server import ProxyServer
|
||||
from .channel_status import ChannelStatus
|
||||
|
|
@ -16,6 +15,7 @@ from .redis_keys import RedisKeys
|
|||
import logging
|
||||
from apps.channels.models import Channel, Stream
|
||||
from apps.m3u.models import M3UAccount, M3UAccountProfile
|
||||
from apps.accounts.models import User
|
||||
from core.models import UserAgent, CoreSettings, PROXY_PROFILE_NAME
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.response import Response
|
||||
|
|
@ -448,13 +448,31 @@ def stream_ts(request, channel_id):
|
|||
|
||||
@api_view(["GET"])
|
||||
def stream_xc(request, username, password, channel_id):
|
||||
user = authenticate(username=username, password=password)
|
||||
if user is None:
|
||||
user = get_object_or_404(User, username=username)
|
||||
|
||||
custom_properties = (
|
||||
json.loads(user.custom_properties) if user.custom_properties else {}
|
||||
)
|
||||
|
||||
if "xc_password" not in custom_properties:
|
||||
return Response({"error": "Invalid credentials"}, status=401)
|
||||
|
||||
channel = get_object_or_404(Channel, id=channel_id)
|
||||
if custom_properties["xc_password"] != password:
|
||||
return Response({"error": "Invalid credentials"}, status=401)
|
||||
|
||||
if user.user_level < 10:
|
||||
channel_profiles = user.channel_profiles.all()
|
||||
filters = {
|
||||
"id": channel_id,
|
||||
"channelprofilemembership__channel_profile__in": channel_profiles,
|
||||
"channelprofilemembership__enabled": True,
|
||||
"user_level__lte": user.user_level,
|
||||
}
|
||||
|
||||
channel = get_object_or_404(Channel, **filters)
|
||||
else:
|
||||
channel = get_object_or_404(Channel, id=channel_id)
|
||||
|
||||
print(channel.uuid)
|
||||
return stream_ts(request._request, channel.uuid)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,12 @@ import {
|
|||
Group,
|
||||
Stack,
|
||||
MultiSelect,
|
||||
Switch,
|
||||
Text,
|
||||
Center,
|
||||
ActionIcon,
|
||||
} from '@mantine/core';
|
||||
import { RotateCcw, X } from 'lucide-react';
|
||||
import { isNotEmpty, useForm } from '@mantine/form';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
import { USER_LEVELS, USER_LEVEL_LABELS } from '../../constants';
|
||||
|
|
@ -28,7 +33,7 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
const profiles = useChannelsStore((s) => s.profiles);
|
||||
const authUser = useAuthStore((s) => s.user);
|
||||
|
||||
console.log(user);
|
||||
const [enableXC, setEnableXC] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
mode: 'uncontrolled',
|
||||
|
|
@ -36,9 +41,8 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
username: '',
|
||||
email: '',
|
||||
user_level: '0',
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_repeat: '',
|
||||
xc_password: '',
|
||||
channel_profiles: [],
|
||||
},
|
||||
|
||||
|
|
@ -57,12 +61,28 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
!values.password.match(/^[a-z0-9]+$/i)
|
||||
? 'Streamer password must be alphanumeric'
|
||||
: null,
|
||||
xc_password:
|
||||
values.xc_password && !values.xc_password.match(/^[a-z0-9]+$/i)
|
||||
? 'XC password must be alphanumeric'
|
||||
: null,
|
||||
}),
|
||||
});
|
||||
|
||||
const onSubmit = async () => {
|
||||
const values = form.getValues();
|
||||
|
||||
const { xc_password, ...customProps } = JSON.parse(
|
||||
user.custom_properties || '{}'
|
||||
);
|
||||
|
||||
if (values.xc_password) {
|
||||
customProps.xc_password = values.xc_password;
|
||||
}
|
||||
|
||||
delete values.xc_password;
|
||||
|
||||
values.custom_properties = JSON.stringify(customProps);
|
||||
|
||||
if (!user) {
|
||||
await API.createUser(values);
|
||||
} else {
|
||||
|
|
@ -79,17 +99,30 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (user?.id) {
|
||||
const customProps = JSON.parse(user.custom_properties || '{}');
|
||||
|
||||
form.setValues({
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
user_level: `${user.user_level}`,
|
||||
channel_profiles: user.channel_profiles.map((id) => `${id}`),
|
||||
xc_password: customProps.xc_password || '',
|
||||
});
|
||||
|
||||
if (customProps.xc_password) {
|
||||
setEnableXC(true);
|
||||
}
|
||||
} else {
|
||||
form.reset();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const generateXCPassword = () => {
|
||||
form.setValues({
|
||||
xc_password: Math.random().toString(36).slice(2),
|
||||
});
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return <></>;
|
||||
}
|
||||
|
|
@ -98,12 +131,7 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
authUser.user_level == USER_LEVELS.ADMIN && authUser.id !== user?.id;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={isOpen}
|
||||
onClose={onClose}
|
||||
title="User"
|
||||
size={showPermissions ? 'xl' : 'md'}
|
||||
>
|
||||
<Modal opened={isOpen} onClose={onClose} title="User" size="xl">
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Group justify="space-between" align="top">
|
||||
<Stack gap="xs" style={{ flex: 1 }}>
|
||||
|
|
@ -115,23 +143,14 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
key={form.key('username')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
id="email"
|
||||
name="email"
|
||||
label="E-Mail"
|
||||
{...form.getInputProps('email')}
|
||||
key={form.key('email')}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
description="Used for UI authentication"
|
||||
{...form.getInputProps('password')}
|
||||
key={form.key('password')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{showPermissions && (
|
||||
<Stack gap="xs" style={{ flex: 1 }}>
|
||||
{showPermissions && (
|
||||
<Select
|
||||
label="User Level"
|
||||
data={Object.entries(USER_LEVELS).map(([label, value]) => {
|
||||
|
|
@ -143,7 +162,40 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
{...form.getInputProps('user_level')}
|
||||
key={form.key('user_level')}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack gap="xs" style={{ flex: 1 }}>
|
||||
<TextInput
|
||||
id="email"
|
||||
name="email"
|
||||
label="E-Mail"
|
||||
{...form.getInputProps('email')}
|
||||
key={form.key('email')}
|
||||
/>
|
||||
|
||||
<Group align="flex-end">
|
||||
<TextInput
|
||||
label="XC Password"
|
||||
description="Auto-generated - clear to disable XC API"
|
||||
{...form.getInputProps('xc_password')}
|
||||
key={form.key('xc_password')}
|
||||
style={{ flex: 1 }}
|
||||
rightSectionWidth={30}
|
||||
rightSection={
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
size="sm"
|
||||
color="white"
|
||||
onClick={generateXCPassword}
|
||||
>
|
||||
<RotateCcw />
|
||||
</ActionIcon>
|
||||
}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{showPermissions && (
|
||||
<MultiSelect
|
||||
label="Channel Profiles"
|
||||
{...form.getInputProps('channel_profiles')}
|
||||
|
|
@ -155,8 +207,8 @@ const User = ({ user = null, isOpen, onClose }) => {
|
|||
value: `${profile.id}`,
|
||||
}))}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
<Flex mih={50} gap="xs" justify="flex-end" align="flex-end">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue