- django-cors-headers with env-driven CORS_ALLOWED_ORIGINS, CORS_ALLOW_ALL_ORIGINS, CORS_ALLOW_CREDENTIALS and CSRF_TRUSTED_ORIGINS; same-origin traffic is unaffected and a disallowed origin gets no CORS headers. Token auth needs no cookies, so credentials stay off by default. - TRUST_PROXY_HEADERS=true lets a TLS-terminating proxy supply X-Forwarded-Proto/Host for correct absolute URLs. - API media URLs (raw/thumbnail/upload/similarity/staged previews) are now absolute, built from the request host, so <img>/<video>/fetch() keep working when the SPA is served from another origin. Signed URLs are still per-user; nothing is stored in the DB. - The SPA gains VITE_API_BASE (build-time, empty = same-origin) applied by a small apiUrl() helper used for XHR/fetch and the few URL fallbacks. Verified with a throwaway instance: preflight and GET responses carry the allowed origin, foreign origins get nothing, media GETs include CORS for cross-origin fetch(), and payload URLs use the request host (dev :8000 unchanged).
130 lines
4.0 KiB
Python
130 lines
4.0 KiB
Python
from pathlib import Path
|
|
|
|
from django.contrib.auth.password_validation import validate_password
|
|
from rest_framework import serializers
|
|
|
|
from apps.library.services import VIDEO_EXTENSIONS
|
|
from apps.library.services import signed_media_url as signed_library_url
|
|
|
|
from .models import User
|
|
|
|
|
|
def signed_media_url(request, item):
|
|
"""A signed media URL an <img> tag can load for an authenticated user."""
|
|
if item is None:
|
|
return None
|
|
user = getattr(request, "user", None)
|
|
if user is None or not getattr(user, "is_authenticated", False):
|
|
return None
|
|
location = item.locations.first()
|
|
is_video = (
|
|
location is not None
|
|
and Path(location.rel_path).suffix.lower() in VIDEO_EXTENSIONS
|
|
)
|
|
action = "thumbnail" if is_video else "raw"
|
|
return signed_library_url(item, user, action, request=request)
|
|
|
|
|
|
class UserSerializer(serializers.ModelSerializer):
|
|
e621_configured = serializers.BooleanField(read_only=True)
|
|
can_upload = serializers.BooleanField(read_only=True)
|
|
avatar_j_id = serializers.SerializerMethodField()
|
|
avatar_url = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = [
|
|
"id",
|
|
"username",
|
|
"email",
|
|
"is_staff",
|
|
"is_superuser",
|
|
"date_joined",
|
|
"role",
|
|
"can_upload",
|
|
"avatar_j_id",
|
|
"avatar_url",
|
|
"e621_username",
|
|
"e621_configured",
|
|
"preferences",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
def get_avatar_j_id(self, obj):
|
|
return f"J-{obj.avatar_id}" if obj.avatar_id else None
|
|
|
|
def get_avatar_url(self, obj):
|
|
return signed_media_url(self.context.get("request"), obj.avatar)
|
|
|
|
|
|
class UserListSerializer(serializers.ModelSerializer):
|
|
avatar_j_id = serializers.SerializerMethodField()
|
|
avatar_url = serializers.SerializerMethodField()
|
|
uploads_count = serializers.IntegerField(read_only=True)
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = [
|
|
"id",
|
|
"username",
|
|
"email",
|
|
"role",
|
|
"is_staff",
|
|
"is_superuser",
|
|
"date_joined",
|
|
"avatar_j_id",
|
|
"avatar_url",
|
|
"uploads_count",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
def get_avatar_j_id(self, obj):
|
|
return f"J-{obj.avatar_id}" if obj.avatar_id else None
|
|
|
|
def get_avatar_url(self, obj):
|
|
return signed_media_url(self.context.get("request"), obj.avatar)
|
|
|
|
|
|
class UserUpdateSerializer(serializers.Serializer):
|
|
avatar_j_id = serializers.CharField(
|
|
required=False, allow_blank=True, allow_null=True
|
|
)
|
|
role = serializers.ChoiceField(choices=User.ROLE_CHOICES, required=False)
|
|
|
|
|
|
class RegisterSerializer(serializers.ModelSerializer):
|
|
password = serializers.CharField(write_only=True, validators=[validate_password])
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = ["username", "email", "password"]
|
|
|
|
def create(self, validated_data):
|
|
return User.objects.create_user(**validated_data)
|
|
|
|
|
|
class E621CredentialsSerializer(serializers.Serializer):
|
|
username = serializers.CharField(max_length=100, allow_blank=True)
|
|
api_key = serializers.CharField(
|
|
max_length=100, allow_blank=True, trim_whitespace=True
|
|
)
|
|
base_url = serializers.URLField(
|
|
max_length=200, required=False, allow_blank=True
|
|
)
|
|
|
|
|
|
class PreferencesSerializer(serializers.Serializer):
|
|
"""Per-user browse preferences; unknown keys are ignored."""
|
|
|
|
LANDING_PAGES = ["library", "online", "pools", "followed"]
|
|
|
|
landing_page = serializers.ChoiceField(choices=LANDING_PAGES, required=False)
|
|
ratings = serializers.ListField(
|
|
child=serializers.ChoiceField(choices=["s", "q", "e"]),
|
|
required=False,
|
|
max_length=3,
|
|
)
|
|
ordering = serializers.CharField(max_length=30, required=False)
|
|
per_page = serializers.IntegerField(min_value=12, max_value=200, required=False)
|
|
zoom = serializers.IntegerField(min_value=120, max_value=400, required=False)
|