Dispatcharr/apps/accounts/serializers.py

149 lines
5 KiB
Python

import json
from rest_framework import serializers
from django.contrib.auth.models import Group, Permission
from .models import User
from apps.channels.models import ChannelProfile
# Valid navigation item IDs for validation
VALID_NAV_ITEM_IDS = {
'channels', 'vods', 'sources', 'guide', 'dvr',
'stats', 'plugins', 'integrations', 'system', 'settings'
}
MAX_CUSTOM_PROPS_SIZE = 102400 # 100KB limit
def validate_nav_array(value, field_name):
"""Validate that a value is an array of valid nav item ID strings."""
if not isinstance(value, list):
raise serializers.ValidationError(f"{field_name} must be an array")
if len(value) > 50:
raise serializers.ValidationError(f"{field_name} exceeds maximum length of 50 items")
for item in value:
if not isinstance(item, str):
raise serializers.ValidationError(f"{field_name} items must be strings")
if item not in VALID_NAV_ITEM_IDS:
raise serializers.ValidationError(f"'{item}' is not a valid navigation item ID")
# 🔹 Fix for Permission serialization
class PermissionSerializer(serializers.ModelSerializer):
class Meta:
model = Permission
fields = ["id", "name", "codename"]
# 🔹 Fix for Group serialization
class GroupSerializer(serializers.ModelSerializer):
permissions = serializers.PrimaryKeyRelatedField(
many=True, queryset=Permission.objects.all()
) # ✅ Fixes ManyToManyField `_meta` error
class Meta:
model = Group
fields = ["id", "name", "permissions"]
# 🔹 Fix for User serialization
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=False)
channel_profiles = serializers.PrimaryKeyRelatedField(
queryset=ChannelProfile.objects.all(), many=True, required=False
)
api_key = serializers.CharField(read_only=True, allow_null=True)
class Meta:
model = User
fields = [
"id",
"username",
"api_key",
"email",
"user_level",
"password",
"channel_profiles",
"custom_properties",
"avatar_config",
"stream_limit",
"is_staff",
"is_superuser",
"last_login",
"date_joined",
"first_name",
"last_name",
]
def validate_custom_properties(self, value):
"""Validate custom_properties structure and size."""
if value is None:
return {}
if not isinstance(value, dict):
raise serializers.ValidationError("custom_properties must be a dictionary")
# Size limit check
try:
if len(json.dumps(value)) > MAX_CUSTOM_PROPS_SIZE:
raise serializers.ValidationError(
f"custom_properties exceeds maximum size of {MAX_CUSTOM_PROPS_SIZE} bytes"
)
except (TypeError, ValueError):
raise serializers.ValidationError("custom_properties contains non-serializable data")
# Validate navOrder if present
if 'navOrder' in value:
validate_nav_array(value['navOrder'], 'navOrder')
# Validate hiddenNav if present
if 'hiddenNav' in value:
validate_nav_array(value['hiddenNav'], 'hiddenNav')
return value
def create(self, validated_data):
channel_profiles = validated_data.pop("channel_profiles", [])
user = User(**validated_data)
user.set_password(validated_data["password"])
user.save()
user.channel_profiles.set(channel_profiles)
return user
def update(self, instance, validated_data):
password = validated_data.pop("password", None)
channel_profiles = validated_data.pop("channel_profiles", None)
# Merge custom_properties instead of replacing (prevents data loss)
# null values are explicit deletions; all other values overwrite existing
custom_properties = validated_data.pop("custom_properties", None)
if custom_properties is not None:
existing = instance.custom_properties or {}
merged = dict(existing)
for k, v in custom_properties.items():
if v is None:
merged.pop(k, None)
else:
merged[k] = v
# Scrub stale nav IDs so the DB self-heals on next save
for nav_field in ('navOrder', 'hiddenNav'):
if nav_field in merged and isinstance(merged[nav_field], list):
merged[nav_field] = [
item for item in merged[nav_field]
if item in VALID_NAV_ITEM_IDS
]
instance.custom_properties = merged
for attr, value in validated_data.items():
setattr(instance, attr, value)
if password:
instance.set_password(password)
instance.save()
if channel_profiles is not None:
instance.channel_profiles.set(channel_profiles)
return instance