From e95c0859ab79cba439909d4b1e429a0cf4c44160 Mon Sep 17 00:00:00 2001 From: dekzter Date: Thu, 22 May 2025 15:21:43 -0400 Subject: [PATCH] user custom properties, xc has its own password, properly checking xc permissions for streaming --- ...l_groups_user_channel_profiles_and_more.py | 5 + apps/accounts/models.py | 1 + apps/accounts/serializers.py | 1 + apps/output/views.py | 48 +++++----- apps/proxy/ts_proxy/views.py | 28 +++++- frontend/src/components/forms/User.jsx | 96 ++++++++++++++----- 6 files changed, 127 insertions(+), 52 deletions(-) diff --git a/apps/accounts/migrations/0002_remove_user_channel_groups_user_channel_profiles_and_more.py b/apps/accounts/migrations/0002_remove_user_channel_groups_user_channel_profiles_and_more.py index 43ad63db..2a095773 100644 --- a/apps/accounts/migrations/0002_remove_user_channel_groups_user_channel_profiles_and_more.py +++ b/apps/accounts/migrations/0002_remove_user_channel_groups_user_channel_profiles_and_more.py @@ -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), ] diff --git a/apps/accounts/models.py b/apps/accounts/models.py index d5b38572..313b20f7 100644 --- a/apps/accounts/models.py +++ b/apps/accounts/models.py @@ -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 diff --git a/apps/accounts/serializers.py b/apps/accounts/serializers.py index 9cebc1fc..5a9f7cef 100644 --- a/apps/accounts/serializers.py +++ b/apps/accounts/serializers.py @@ -38,6 +38,7 @@ class UserSerializer(serializers.ModelSerializer): "user_level", "password", "channel_profiles", + "custom_properties", ] def create(self, validated_data): diff --git a/apps/output/views.py b/apps/output/views.py index c51e4472..e666043f 100644 --- a/apps/output/views.py +++ b/apps/output/views.py @@ -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() diff --git a/apps/proxy/ts_proxy/views.py b/apps/proxy/ts_proxy/views.py index facd441c..ce363056 100644 --- a/apps/proxy/ts_proxy/views.py +++ b/apps/proxy/ts_proxy/views.py @@ -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) diff --git a/frontend/src/components/forms/User.jsx b/frontend/src/components/forms/User.jsx index 5a483fbd..b99949be 100644 --- a/frontend/src/components/forms/User.jsx +++ b/frontend/src/components/forms/User.jsx @@ -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 ( - +
@@ -115,23 +143,14 @@ const User = ({ user = null, isOpen, onClose }) => { key={form.key('username')} /> - - - - {showPermissions && ( - + {showPermissions && (