diff --git a/apps/podcast/serializers.py b/apps/podcast/serializers.py index 72ebacf..9b8f106 100755 --- a/apps/podcast/serializers.py +++ b/apps/podcast/serializers.py @@ -16,6 +16,29 @@ class PodcastCategoryListSerializer(serializers.ModelSerializer): return obj.playlists.filter(status=True, playlist_items__podcast__status=True).distinct().count() +def build_podcast_audio_url(obj, request=None): + """ + Build absolute HTTPS URL for podcast audio file with cache-busting query parameter. + Bypasses poisoned Cloudflare 301 cache redirects and enforces HTTPS. + """ + if not obj.audio_file: + return None + url = obj.audio_file.url + if request: + full_url = request.build_absolute_uri(url) + else: + full_url = url + + # 1. Force HTTPS protocol (except local dev) + if full_url.startswith('http://') and 'localhost' not in full_url and '127.0.0.1' not in full_url: + full_url = 'https://' + full_url[7:] + + # 2. Bypass poisoned Cloudflare 301 redirect cache + separator = '&' if '?' in full_url else '?' + version = int(obj.updated_at.timestamp()) if hasattr(obj, 'updated_at') and obj.updated_at else getattr(obj, 'id', 1) + return f"{full_url}{separator}v={version}" + + class PodcastListSerializer(serializers.ModelSerializer): thumbnail = serializers.SerializerMethodField() audio_file = serializers.SerializerMethodField() @@ -32,13 +55,8 @@ class PodcastListSerializer(serializers.ModelSerializer): return get_thumbs(obj.thumbnail, self.context.get('request')) def get_audio_file(self, obj): - """Get full URL for audio file if it exists""" - if obj.audio_file: - request = self.context.get('request') - if request: - return request.build_absolute_uri(obj.audio_file.url) - return obj.audio_file.url - return None + """Get full URL for audio file if it exists with HTTPS and cache-buster""" + return build_podcast_audio_url(obj, self.context.get('request')) def get_audio_time(self, obj): return format_media_time(obj.audio_time) @@ -68,6 +86,7 @@ class PodcastDetailSerializer(serializers.ModelSerializer): in_user_playlist = serializers.SerializerMethodField() share_link = serializers.CharField(read_only=True) audio_time = serializers.SerializerMethodField() + audio_file = serializers.SerializerMethodField() class Meta: model = Podcast fields = ['id', 'title', 'slug', 'thumbnail', 'description', @@ -75,6 +94,10 @@ class PodcastDetailSerializer(serializers.ModelSerializer): 'categories', 'created_at', 'user_rate', 'average_rate', 'bookmark', 'is_in_playlist', 'playlist_podcasts', 'in_user_playlist', 'share_link'] + def get_audio_file(self, obj): + """Get full URL for audio file if it exists with HTTPS and cache-buster""" + return build_podcast_audio_url(obj, self.context.get('request')) + def get_thumbnail(self, obj): return get_thumbs(obj.thumbnail, self.context.get('request')) diff --git a/apps/podcast/serializers_admin.py b/apps/podcast/serializers_admin.py index 4f9ca7b..5595f60 100644 --- a/apps/podcast/serializers_admin.py +++ b/apps/podcast/serializers_admin.py @@ -34,9 +34,7 @@ class AbsoluteFileField(serializers.FileField): return None request = self.context.get("request") url = value.url if hasattr(value, "url") else str(value) - if request: - return request.build_absolute_uri(url) - return url + return absolute_https_url(url, request) class AdminPodcastCategorySerializer(serializers.ModelSerializer): diff --git a/config/settings/base.py b/config/settings/base.py index e76b254..9ea72d1 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -31,6 +31,11 @@ environ.Env.read_env(os.path.join(BASE_DIR, '.env')) ALLOWED_HOSTS = env('DJANGO_ALLOWED_HOSTS').split(',') CSRF_TRUSTED_ORIGINS = env('DJANGO_TRUSTED_ORIGINS').split(',') +# Reverse Proxy / SSL settings (Cloudflare & Nginx) +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +USE_X_FORWARDED_HOST = True +USE_X_FORWARDED_PORT = True + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ diff --git a/config/settings/production.py b/config/settings/production.py index 79a9e78..9185dd4 100644 --- a/config/settings/production.py +++ b/config/settings/production.py @@ -9,6 +9,8 @@ DEBUG = False CORS_ALLOW_ALL_ORIGINS = True SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +USE_X_FORWARDED_HOST = True +USE_X_FORWARDED_PORT = True diff --git a/nginx/dovodi.conf b/nginx/dovodi.conf index a12d884..a7c59d5 100644 --- a/nginx/dovodi.conf +++ b/nginx/dovodi.conf @@ -1,6 +1,19 @@ +# 1. Port 80 (HTTP) Server Block - Redirect all HTTP traffic to HTTPS server { listen 80; - listen 443 ssl; + listen [::]:80; + server_name dovodi.nwhco.ir dovodi.newhorizonco.uk dovodi.com test.dovodi.com; + + # Immediate 301 redirect to HTTPS without caching redirect + location / { + return 301 https://$host$request_uri; + } +} + +# 2. Port 443 (HTTPS) Server Block +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; server_name dovodi.nwhco.ir dovodi.newhorizonco.uk dovodi.com test.dovodi.com; ssl_certificate /etc/nginx/certs/nwhco.pem; ssl_certificate_key /etc/nginx/certs/nwhco.key; @@ -21,9 +34,8 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; client_max_body_size 1200M; - } location /fa/dovoodi/ { @@ -31,7 +43,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; client_max_body_size 1200M; } @@ -41,7 +53,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; client_max_body_size 1200M; } @@ -54,7 +66,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; client_max_body_size 1200M; } @@ -64,7 +76,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; client_max_body_size 1200M; } @@ -74,7 +86,7 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; proxy_set_header Authorization $http_authorization; proxy_pass_header Authorization; client_max_body_size 500M; @@ -90,7 +102,7 @@ server { proxy_set_header Host $host; proxy_method $request_method; proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; proxy_set_header Authorization $http_authorization; proxy_pass_header Authorization; @@ -121,21 +133,28 @@ server { # ========== Static & Media Files ========== location /static/ { - alias /home/app/web/imam_javad_staticfiles/; + alias /home/app/web/imam_javad_staticfiles/; expires 30d; add_header Cache-Control "public, immutable"; } - location /media/{ - alias /home/app/web/imam_javad_mediafiles/; + location /media/ { + alias /home/app/web/imam_javad_mediafiles/; + + # Range Requests for audio streaming / seeking in player + add_header Accept-Ranges bytes; + # CORS headers for media streaming + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods 'GET, HEAD, OPTIONS' always; + add_header Access-Control-Allow-Headers 'Range, Origin, Content-Type, Accept' always; + + # Standard static cache expires 30d; - add_header Cache-Control "public, immutable"; + add_header Cache-Control "public, no-transform"; - # Enable CORS for media files - add_header Access-Control-Allow-Origin *; - add_header Access-Control-Allow-Methods "GET, OPTIONS"; - add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept"; + access_log off; + log_not_found off; } # ========== Next.js Frontend ==========