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