mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
check and warn before saving a network access setting that could block current client access
This commit is contained in:
parent
789d29c97a
commit
82f35d2aef
5 changed files with 148 additions and 29 deletions
|
|
@ -11,6 +11,10 @@
|
|||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.history
|
||||
**/media
|
||||
**/models
|
||||
**/static
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
|
|
@ -26,3 +30,4 @@
|
|||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
data/
|
||||
|
|
|
|||
|
|
@ -3,33 +3,45 @@ from rest_framework.response import Response
|
|||
from .models import M3UAccount, M3UFilter, ServerGroup, M3UAccountProfile
|
||||
from core.models import UserAgent
|
||||
from apps.channels.models import ChannelGroup, ChannelGroupM3UAccount
|
||||
from apps.channels.serializers import ChannelGroupM3UAccountSerializer, ChannelGroupSerializer
|
||||
from apps.channels.serializers import (
|
||||
ChannelGroupM3UAccountSerializer,
|
||||
ChannelGroupSerializer,
|
||||
)
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class M3UFilterSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for M3U Filters"""
|
||||
channel_groups = ChannelGroupM3UAccountSerializer(source='m3u_account', many=True)
|
||||
|
||||
channel_groups = ChannelGroupM3UAccountSerializer(source="m3u_account", many=True)
|
||||
|
||||
class Meta:
|
||||
model = M3UFilter
|
||||
fields = ['id', 'filter_type', 'regex_pattern', 'exclude', 'channel_groups']
|
||||
fields = ["id", "filter_type", "regex_pattern", "exclude", "channel_groups"]
|
||||
|
||||
from rest_framework import serializers
|
||||
from .models import M3UAccountProfile
|
||||
|
||||
class M3UAccountProfileSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = M3UAccountProfile
|
||||
fields = ['id', 'name', 'max_streams', 'is_active', 'is_default', 'current_viewers', 'search_pattern', 'replace_pattern']
|
||||
read_only_fields = ['id']
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"max_streams",
|
||||
"is_active",
|
||||
"is_default",
|
||||
"current_viewers",
|
||||
"search_pattern",
|
||||
"replace_pattern",
|
||||
]
|
||||
read_only_fields = ["id"]
|
||||
|
||||
def create(self, validated_data):
|
||||
m3u_account = self.context.get('m3u_account')
|
||||
m3u_account = self.context.get("m3u_account")
|
||||
|
||||
# Use the m3u_account when creating the profile
|
||||
validated_data['m3u_account_id'] = m3u_account.id
|
||||
validated_data["m3u_account_id"] = m3u_account.id
|
||||
|
||||
return super().create(validated_data)
|
||||
|
||||
|
|
@ -43,12 +55,14 @@ class M3UAccountProfileSerializer(serializers.ModelSerializer):
|
|||
if instance.is_default:
|
||||
return Response(
|
||||
{"error": "Default profiles cannot be deleted."},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
class M3UAccountSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for M3U Account"""
|
||||
|
||||
filters = M3UFilterSerializer(many=True, read_only=True)
|
||||
# Include user_agent as a mandatory field using its primary key.
|
||||
user_agent = serializers.PrimaryKeyRelatedField(
|
||||
|
|
@ -57,28 +71,48 @@ class M3UAccountSerializer(serializers.ModelSerializer):
|
|||
allow_null=True,
|
||||
)
|
||||
profiles = M3UAccountProfileSerializer(many=True, read_only=True)
|
||||
read_only_fields = ['locked', 'created_at', 'updated_at']
|
||||
read_only_fields = ["locked", "created_at", "updated_at"]
|
||||
# channel_groups = serializers.SerializerMethodField()
|
||||
channel_groups = ChannelGroupM3UAccountSerializer(source='channel_group', many=True, required=False)
|
||||
channel_groups = ChannelGroupM3UAccountSerializer(
|
||||
source="channel_group", many=True, required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = M3UAccount
|
||||
fields = [
|
||||
'id', 'name', 'server_url', 'file_path', 'server_group',
|
||||
'max_streams', 'is_active', 'created_at', 'updated_at', 'filters', 'user_agent', 'profiles', 'locked',
|
||||
'channel_groups', 'refresh_interval', 'custom_properties', 'account_type', 'username', 'password', 'stale_stream_days',
|
||||
'status', 'last_message',
|
||||
"id",
|
||||
"name",
|
||||
"server_url",
|
||||
"file_path",
|
||||
"server_group",
|
||||
"max_streams",
|
||||
"is_active",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"filters",
|
||||
"user_agent",
|
||||
"profiles",
|
||||
"locked",
|
||||
"channel_groups",
|
||||
"refresh_interval",
|
||||
"custom_properties",
|
||||
"account_type",
|
||||
"username",
|
||||
"password",
|
||||
"stale_stream_days",
|
||||
"status",
|
||||
"last_message",
|
||||
]
|
||||
extra_kwargs = {
|
||||
'password': {
|
||||
'required': False,
|
||||
'allow_blank': True,
|
||||
"password": {
|
||||
"required": False,
|
||||
"allow_blank": True,
|
||||
},
|
||||
}
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# Pop out channel group memberships so we can handle them manually
|
||||
channel_group_data = validated_data.pop('channel_group', [])
|
||||
channel_group_data = validated_data.pop("channel_group", [])
|
||||
|
||||
# First, update the M3UAccount itself
|
||||
for attr, value in validated_data.items():
|
||||
|
|
@ -88,13 +122,12 @@ class M3UAccountSerializer(serializers.ModelSerializer):
|
|||
# Prepare a list of memberships to update
|
||||
memberships_to_update = []
|
||||
for group_data in channel_group_data:
|
||||
group = group_data.get('channel_group')
|
||||
enabled = group_data.get('enabled')
|
||||
group = group_data.get("channel_group")
|
||||
enabled = group_data.get("enabled")
|
||||
|
||||
try:
|
||||
membership = ChannelGroupM3UAccount.objects.get(
|
||||
m3u_account=instance,
|
||||
channel_group=group
|
||||
m3u_account=instance, channel_group=group
|
||||
)
|
||||
membership.enabled = enabled
|
||||
memberships_to_update.append(membership)
|
||||
|
|
@ -103,13 +136,16 @@ class M3UAccountSerializer(serializers.ModelSerializer):
|
|||
|
||||
# Perform the bulk update
|
||||
if memberships_to_update:
|
||||
ChannelGroupM3UAccount.objects.bulk_update(memberships_to_update, ['enabled'])
|
||||
ChannelGroupM3UAccount.objects.bulk_update(
|
||||
memberships_to_update, ["enabled"]
|
||||
)
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class ServerGroupSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for Server Group"""
|
||||
|
||||
class Meta:
|
||||
model = ServerGroup
|
||||
fields = ['id', 'name']
|
||||
fields = ["id", "name"]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# core/api_views.py
|
||||
|
||||
import json
|
||||
import ipaddress
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.response import Response
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
|
@ -9,7 +11,7 @@ from .serializers import (
|
|||
StreamProfileSerializer,
|
||||
CoreSettingsSerializer,
|
||||
)
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.decorators import api_view, permission_classes, action
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
import socket
|
||||
import requests
|
||||
|
|
@ -18,6 +20,7 @@ from core.tasks import rehash_streams
|
|||
from apps.accounts.permissions import (
|
||||
Authenticated,
|
||||
)
|
||||
from dispatcharr.utils import get_client_ip
|
||||
|
||||
|
||||
class UserAgentViewSet(viewsets.ModelViewSet):
|
||||
|
|
@ -56,6 +59,23 @@ class CoreSettingsViewSet(viewsets.ModelViewSet):
|
|||
|
||||
return response
|
||||
|
||||
@action(detail=False, methods=["post"], url_path="check")
|
||||
def check(self, request, *args, **kwargs):
|
||||
data = request.data
|
||||
|
||||
client_ip = ipaddress.ip_address(get_client_ip(request))
|
||||
in_network = []
|
||||
key = data.get("key")
|
||||
value = json.loads(data.get("value", "{}"))
|
||||
for key, val in value.items():
|
||||
cidrs = val.split(",")
|
||||
for cidr in cidrs:
|
||||
network = ipaddress.ip_network(cidr)
|
||||
if client_ip not in network:
|
||||
in_network.append(cidr)
|
||||
|
||||
return Response(in_network, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@swagger_auto_schema(
|
||||
method="get",
|
||||
|
|
|
|||
|
|
@ -1085,6 +1085,21 @@ export default class API {
|
|||
}
|
||||
}
|
||||
|
||||
static async checkSetting(values) {
|
||||
const { id, ...payload } = values;
|
||||
|
||||
try {
|
||||
const response = await request(`${host}/api/core/settings/check/`, {
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to update settings', e);
|
||||
}
|
||||
}
|
||||
|
||||
static async updateSetting(values) {
|
||||
const { id, ...payload } = values;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import StreamProfilesTable from '../components/tables/StreamProfilesTable';
|
|||
import useLocalStorage from '../hooks/useLocalStorage';
|
||||
import useAuthStore from '../store/auth';
|
||||
import { USER_LEVELS, NETWORK_ACCESS_OPTIONS } from '../constants';
|
||||
import ConfirmationDialog from '../components/ConfirmationDialog';
|
||||
|
||||
const SettingsPage = () => {
|
||||
const settings = useSettingsStore((s) => s.settings);
|
||||
|
|
@ -33,6 +34,10 @@ const SettingsPage = () => {
|
|||
|
||||
const [accordianValue, setAccordianValue] = useState(null);
|
||||
const [networkAccessSaved, setNetworkAccessSaved] = useState(false);
|
||||
const [networkAccessConfirmOpen, setNetworkAccessConfirmOpen] =
|
||||
useState(false);
|
||||
const [netNetworkAccessConfirmCIDRs, setNetNetworkAccessConfirmCIDRs] =
|
||||
useState([]);
|
||||
|
||||
// UI / local storage settings
|
||||
const [tableSize, setTableSize] = useLocalStorage('table-size', 'default');
|
||||
|
|
@ -315,7 +320,6 @@ const SettingsPage = () => {
|
|||
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
console.log(settings);
|
||||
const formValues = Object.entries(settings).reduce(
|
||||
(acc, [key, value]) => {
|
||||
// Modify each value based on its own properties
|
||||
|
|
@ -378,7 +382,21 @@ const SettingsPage = () => {
|
|||
};
|
||||
|
||||
const onNetworkAccessSubmit = async () => {
|
||||
let result = null;
|
||||
setNetworkAccessSaved(false);
|
||||
const check = await API.checkSetting({
|
||||
...settings['network-access'],
|
||||
value: JSON.stringify(networkAccessForm.getValues()),
|
||||
});
|
||||
|
||||
if (check.length == 0) {
|
||||
return saveNetworkAccess();
|
||||
}
|
||||
|
||||
setNetNetworkAccessConfirmCIDRs(check);
|
||||
setNetworkAccessConfirmOpen(true);
|
||||
};
|
||||
|
||||
const saveNetworkAccess = async () => {
|
||||
setNetworkAccessSaved(false);
|
||||
try {
|
||||
await API.updateSetting({
|
||||
|
|
@ -386,6 +404,7 @@ const SettingsPage = () => {
|
|||
value: JSON.stringify(networkAccessForm.getValues()),
|
||||
});
|
||||
setNetworkAccessSaved(true);
|
||||
setNetworkAccessConfirmOpen(false);
|
||||
} catch (e) {
|
||||
const errors = {};
|
||||
for (const key in e.body.value) {
|
||||
|
|
@ -644,6 +663,30 @@ const SettingsPage = () => {
|
|||
)}
|
||||
</Accordion>
|
||||
</Box>
|
||||
|
||||
<ConfirmationDialog
|
||||
opened={networkAccessConfirmOpen}
|
||||
onClose={() => setNetworkAccessConfirmOpen(false)}
|
||||
onConfirm={saveNetworkAccess}
|
||||
title={`Confirm Network Access Blocks`}
|
||||
message={
|
||||
<>
|
||||
<Text>
|
||||
Your client is included in the following CIDRs and could block
|
||||
access Are you sure you want to proceed?
|
||||
</Text>
|
||||
|
||||
<ul>
|
||||
{netNetworkAccessConfirmCIDRs.map((cidr) => (
|
||||
<li>{cidr}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
confirmLabel="Save"
|
||||
cancelLabel="Cancel"
|
||||
size="md"
|
||||
/>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue