user custom properties, xc has its own password, properly checking xc permissions for streaming

This commit is contained in:
dekzter 2025-05-22 15:21:43 -04:00
parent e3553b04ad
commit e95c0859ab
6 changed files with 127 additions and 52 deletions

View file

@ -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),
]

View file

@ -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

View file

@ -38,6 +38,7 @@ class UserSerializer(serializers.ModelSerializer):
"user_level",
"password",
"channel_profiles",
"custom_properties",
]
def create(self, validated_data):

View file

@ -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()

View file

@ -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)

View file

@ -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">