mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
commit
06358a0333
12 changed files with 142 additions and 28 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -9,7 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Security
|
||||
|
||||
- Set `DEFAULT_PERMISSION_CLASSES` to `IsAdmin` in the DRF configuration. All viewsets and function-based views that require non-admin or unauthenticated access were explicitly annotated: proxy streaming endpoints (`stream_ts`, `stream_xc`, `stream_vod`, `head_vod`, `stream_xc_movie`, `stream_xc_episode`) use `@permission_classes([AllowAny])` (access is controlled by the per-stream-type network allow-list inside the view body); the `UserAgentViewSet`, `StreamProfileViewSet`, `CoreSettingsViewSet`, and `ProxySettingsViewSet` gained `get_permissions()` methods mapping read actions to `IsStandardUser` and write actions to `IsAdmin`; and `AuthViewSet.logout` was updated to return `[Authenticated()]`.
|
||||
- Fixed missing `network_access_allowed` checks in the VOD proxy. `stream_vod`, `head_vod`, `stream_xc_movie`, and `stream_xc_episode` were not checking the `STREAMS` network policy, unlike the equivalent TS proxy endpoints.
|
||||
- Explicitly marked the HDHomeRun discovery endpoints (`DiscoverAPIView`, `LineupAPIView`, `LineupStatusAPIView`, `HDHRDeviceXMLAPIView`) and the version endpoint with `permission_classes = [AllowAny]` to document their intentionally public access now that the global default is `IsAdmin`.
|
||||
- Fixed path traversal vulnerability in file uploads. The M3U account upload (`apps/m3u/api_views.py`), logo upload (`apps/channels/api_views.py`), and backup upload (`apps/backups/api_views.py`) all used the uploaded filename directly without sanitization. `os.path.join()` discards all preceding components when it encounters an absolute path segment, and `pathlib`'s `/` operator behaves identically; a relative `../` sequence also escapes via OS path resolution at `open()` time. All three upload paths now strip directory components via `Path(name).name` and validate the resolved path remains within the intended upload directory. Exploiting any of these required admin credentials.
|
||||
- Prevented users from setting `xc_password` (and other admin-managed keys) on their own account via the `PATCH /api/accounts/users/me/` endpoint.
|
||||
- Hardened the HLS proxy `change_stream` endpoint by converting it from a plain Django view to a DRF `@api_view` with `@permission_classes([IsAdmin])`, ensuring the endpoint actually enforces admin-only access. The previous decorator arrangement (`@csrf_exempt` + `@permission_classes`) had no effect on a plain Django view.
|
||||
- Added rate limiting to the login endpoint (`POST /api/accounts/token/`) using DRF's built-in throttling. A `LoginRateThrottle` (3 requests/minute per IP, sliding window) is applied to the `TokenObtainPairView`. Repeated failed attempts from the same IP receive `429 Too Many Requests`.
|
||||
- Removed `CORS_ALLOW_CREDENTIALS = True` from CORS configuration. Dispatcharr authenticates via JWT `Authorization` headers and API keys — not cookies — so credentials are never sent cross-origin by browsers. The setting was also redundant: browsers reject `Access-Control-Allow-Credentials: true` when `Access-Control-Allow-Origin` is a wildcard (`*`), so it had no effect in practice.
|
||||
- Updated frontend npm dependencies to resolve 6 audit vulnerabilities (6 high):
|
||||
- Updated `@xmldom/xmldom` 0.8.11 → 0.8.12, resolving **high** XML injection via unsafe CDATA serialization allowing attacker-controlled markup insertion ([GHSA-wh4c-j3r5-mjhp](https://github.com/advisories/GHSA-wh4c-j3r5-mjhp))
|
||||
- Updated `lodash` 4.17.23 → 4.18.1, resolving **high** Code Injection via `_.template` imports key names ([GHSA-r5fr-rjxr-66jc](https://github.com/advisories/GHSA-r5fr-rjxr-66jc)) and **high** Prototype Pollution via array path bypass in `_.unset` and `_.omit` ([GHSA-f23m-r3pf-42rh](https://github.com/advisories/GHSA-f23m-r3pf-42rh))
|
||||
- Updated `vite` 7.3.1 → 7.3.2, resolving **high** Path Traversal in optimized deps `.map` handling ([GHSA-4w7w-66w2-5vf9](https://github.com/advisories/GHSA-4w7w-66w2-5vf9)), **high** `server.fs.deny` bypass with queries ([GHSA-v2wj-q39q-566r](https://github.com/advisories/GHSA-v2wj-q39q-566r)), and **high** Arbitrary File Read via dev server WebSocket ([GHSA-p9ff-h696-f583](https://github.com/advisories/GHSA-p9ff-h696-f583))
|
||||
|
||||
### Removed
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from django.views.decorators.csrf import csrf_exempt
|
|||
from rest_framework.decorators import api_view, permission_classes, action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import viewsets, status, serializers
|
||||
from rest_framework.throttling import AnonRateThrottle
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter, inline_serializer
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
import json
|
||||
|
|
@ -20,9 +21,14 @@ from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoginRateThrottle(AnonRateThrottle):
|
||||
scope = "login"
|
||||
|
||||
|
||||
class TokenObtainPairView(TokenObtainPairView):
|
||||
throttle_classes = [LoginRateThrottle]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# Custom logic here
|
||||
if not network_access_allowed(request, "UI"):
|
||||
# Log blocked login attempt due to network restrictions
|
||||
from core.utils import log_system_event
|
||||
|
|
@ -153,8 +159,7 @@ class AuthViewSet(viewsets.ViewSet):
|
|||
Login doesn't require auth, but logout does
|
||||
"""
|
||||
if self.action == 'logout':
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
return [IsAuthenticated()]
|
||||
return [Authenticated()]
|
||||
return []
|
||||
|
||||
@extend_schema(
|
||||
|
|
@ -291,6 +296,14 @@ class UserViewSet(viewsets.ModelViewSet):
|
|||
for key in disallowed:
|
||||
request.data.pop(key, None)
|
||||
|
||||
# Strip admin-managed keys from custom_properties so users cannot
|
||||
# set their own XC credentials via this endpoint.
|
||||
ADMIN_ONLY_PROPS = {"xc_password"}
|
||||
cp = request.data.get("custom_properties")
|
||||
if isinstance(cp, dict):
|
||||
for key in ADMIN_ONLY_PROPS:
|
||||
cp.pop(key, None)
|
||||
|
||||
serializer = UserSerializer(user, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from rest_framework.permissions import AllowAny
|
|||
from apps.accounts.permissions import IsAdmin
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from rest_framework.response import Response
|
||||
from core.utils import safe_upload_path
|
||||
|
||||
from . import services
|
||||
from .tasks import create_backup_task, restore_backup_task
|
||||
|
|
@ -267,10 +268,18 @@ def upload_backup(request):
|
|||
|
||||
try:
|
||||
backup_dir = services.get_backup_dir()
|
||||
filename = uploaded.name or "uploaded-backup.zip"
|
||||
# Sanitize filename: strip directory components to prevent path traversal
|
||||
filename = Path(uploaded.name or "uploaded-backup.zip").name
|
||||
if not filename:
|
||||
filename = "uploaded-backup.zip"
|
||||
|
||||
try:
|
||||
safe_upload_path(filename, str(backup_dir))
|
||||
except ValueError:
|
||||
return Response({"detail": "Invalid filename."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Ensure unique filename
|
||||
backup_file = backup_dir / filename
|
||||
backup_file = (backup_dir / filename).resolve()
|
||||
counter = 1
|
||||
while backup_file.exists():
|
||||
name_parts = filename.rsplit(".", 1)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ from apps.accounts.permissions import (
|
|||
)
|
||||
|
||||
from core.models import UserAgent, CoreSettings
|
||||
from core.utils import RedisClient
|
||||
from core.utils import RedisClient, safe_upload_path
|
||||
|
||||
from .models import (
|
||||
Stream,
|
||||
|
|
@ -1901,10 +1901,13 @@ class LogoViewSet(viewsets.ModelViewSet):
|
|||
{"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
file_name = file.name
|
||||
file_path = os.path.join("/data/logos", file_name)
|
||||
# Sanitize filename: strip directory components to prevent path traversal
|
||||
try:
|
||||
file_path = safe_upload_path(file.name, "/data/logos")
|
||||
except ValueError:
|
||||
return Response({"error": "Invalid filename."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
os.makedirs("/data/logos", exist_ok=True)
|
||||
with open(file_path, "wb+") as destination:
|
||||
for chunk in file.chunks():
|
||||
destination.write(chunk)
|
||||
|
|
@ -1924,7 +1927,7 @@ class LogoViewSet(viewsets.ModelViewSet):
|
|||
|
||||
# Get custom name from request data, fallback to filename
|
||||
custom_name = request.data.get('name', '').strip()
|
||||
logo_name = custom_name if custom_name else file_name
|
||||
logo_name = custom_name if custom_name else os.path.basename(file_path)
|
||||
|
||||
logo, _ = Logo.objects.get_or_create(
|
||||
url=file_path,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from rest_framework import viewsets, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import AllowAny
|
||||
from apps.accounts.permissions import Authenticated, permission_classes_by_action
|
||||
from django.http import JsonResponse, HttpResponseForbidden, HttpResponse
|
||||
import logging
|
||||
|
|
@ -46,6 +47,7 @@ class HDHRDeviceViewSet(viewsets.ModelViewSet):
|
|||
# 🔹 2) Discover API
|
||||
class DiscoverAPIView(APIView):
|
||||
"""Returns device discovery information"""
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
description="Retrieve HDHomeRun device discovery information",
|
||||
|
|
@ -98,6 +100,7 @@ class DiscoverAPIView(APIView):
|
|||
# 🔹 3) Lineup API
|
||||
class LineupAPIView(APIView):
|
||||
"""Returns available channel lineup"""
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
description="Retrieve the available channel lineup",
|
||||
|
|
@ -138,6 +141,7 @@ class LineupAPIView(APIView):
|
|||
# 🔹 4) Lineup Status API
|
||||
class LineupStatusAPIView(APIView):
|
||||
"""Returns the current status of the HDHR lineup"""
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
description="Retrieve the HDHomeRun lineup status",
|
||||
|
|
@ -155,6 +159,7 @@ class LineupStatusAPIView(APIView):
|
|||
# 🔹 5) Device XML API
|
||||
class HDHRDeviceXMLAPIView(APIView):
|
||||
"""Returns HDHomeRun device configuration in XML"""
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
description="Retrieve the HDHomeRun device XML configuration",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import json
|
|||
|
||||
from .models import M3UAccount, M3UFilter, ServerGroup, M3UAccountProfile
|
||||
from core.models import UserAgent
|
||||
from core.utils import safe_upload_path
|
||||
from apps.channels.models import ChannelGroupM3UAccount
|
||||
from core.serializers import UserAgentSerializer
|
||||
from apps.vod.models import M3UVODCategoryRelation
|
||||
|
|
@ -54,10 +55,12 @@ class M3UAccountViewSet(viewsets.ModelViewSet):
|
|||
file_path = None
|
||||
if "file" in request.FILES:
|
||||
file = request.FILES["file"]
|
||||
file_name = file.name
|
||||
file_path = os.path.join("/data/uploads/m3us", file_name)
|
||||
try:
|
||||
file_path = safe_upload_path(file.name, "/data/uploads/m3us")
|
||||
except ValueError:
|
||||
return Response({"detail": "Invalid filename."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
os.makedirs("/data/uploads/m3us", exist_ok=True)
|
||||
with open(file_path, "wb+") as destination:
|
||||
for chunk in file.chunks():
|
||||
destination.write(chunk)
|
||||
|
|
@ -117,10 +120,12 @@ class M3UAccountViewSet(viewsets.ModelViewSet):
|
|||
file_path = None
|
||||
if "file" in request.FILES:
|
||||
file = request.FILES["file"]
|
||||
file_name = file.name
|
||||
file_path = os.path.join("/data/uploads/m3us", file_name)
|
||||
try:
|
||||
file_path = safe_upload_path(file.name, "/data/uploads/m3us")
|
||||
except ValueError:
|
||||
return Response({"detail": "Invalid filename."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
os.makedirs("/data/uploads/m3us", exist_ok=True)
|
||||
with open(file_path, "wb+") as destination:
|
||||
for chunk in file.chunks():
|
||||
destination.write(chunk)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from apps.m3u.models import M3UAccount, M3UAccountProfile
|
|||
from apps.accounts.models import User
|
||||
from core.models import UserAgent, CoreSettings, PROXY_PROFILE_NAME
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from apps.accounts.permissions import (
|
||||
IsAdmin,
|
||||
|
|
@ -46,6 +47,7 @@ logger = get_logger()
|
|||
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([AllowAny])
|
||||
def stream_ts(request, channel_id, user=None):
|
||||
if not network_access_allowed(request, "STREAMS"):
|
||||
return JsonResponse({"error": "Forbidden"}, status=403)
|
||||
|
|
@ -554,6 +556,7 @@ def stream_ts(request, channel_id, user=None):
|
|||
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([AllowAny])
|
||||
def stream_xc(request, username, password, channel_id):
|
||||
user = get_object_or_404(User, username=username)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,9 +15,11 @@ from apps.m3u.models import M3UAccountProfile
|
|||
from apps.proxy.vod_proxy.multi_worker_connection_manager import MultiWorkerVODConnectionManager, infer_content_type_from_url, get_vod_client_stop_key
|
||||
from .utils import get_client_info
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import AllowAny
|
||||
from apps.accounts.models import User
|
||||
from apps.accounts.permissions import IsAdmin
|
||||
from apps.proxy.utils import check_user_stream_limits
|
||||
from dispatcharr.utils import network_access_allowed
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -291,6 +293,7 @@ def _transform_url(original_url, m3u_profile):
|
|||
return original_url
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([AllowAny])
|
||||
def stream_vod(request, content_type, content_id, session_id=None, profile_id=None, user=None):
|
||||
"""
|
||||
Stream VOD content (movies or series episodes) with session-based connection reuse
|
||||
|
|
@ -301,6 +304,9 @@ def stream_vod(request, content_type, content_id, session_id=None, profile_id=No
|
|||
session_id: Optional session ID from URL path (for persistent connections)
|
||||
profile_id: Optional M3U profile ID for authentication
|
||||
"""
|
||||
if not network_access_allowed(request, "STREAMS"):
|
||||
return JsonResponse({"error": "Forbidden"}, status=403)
|
||||
|
||||
logger.info(f"[VOD-REQUEST] Starting VOD stream request: {content_type}/{content_id}, session: {session_id}, profile: {profile_id}")
|
||||
logger.info(f"[VOD-REQUEST] Full request path: {request.get_full_path()}")
|
||||
logger.info(f"[VOD-REQUEST] Request method: {request.method}")
|
||||
|
|
@ -500,12 +506,16 @@ def stream_vod(request, content_type, content_id, session_id=None, profile_id=No
|
|||
return HttpResponse(f"Streaming error: {str(e)}", status=500)
|
||||
|
||||
@api_view(["HEAD"])
|
||||
@permission_classes([AllowAny])
|
||||
def head_vod(request, content_type, content_id, session_id=None, profile_id=None):
|
||||
"""
|
||||
Handle HEAD requests for FUSE filesystem integration
|
||||
|
||||
Returns content length and session URL header for subsequent GET requests
|
||||
"""
|
||||
if not network_access_allowed(request, "STREAMS"):
|
||||
return JsonResponse({"error": "Forbidden"}, status=403)
|
||||
|
||||
logger.info(f"[VOD-HEAD] HEAD request: {content_type}/{content_id}, session: {session_id}, profile: {profile_id}")
|
||||
|
||||
try:
|
||||
|
|
@ -987,7 +997,11 @@ def stop_vod_client(request):
|
|||
return JsonResponse({'error': str(e)}, status=500)
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([AllowAny])
|
||||
def stream_xc_movie(request, username, password, stream_id, extension):
|
||||
if not network_access_allowed(request, "STREAMS"):
|
||||
return JsonResponse({"error": "Forbidden"}, status=403)
|
||||
|
||||
from apps.vod.models import M3UMovieRelation
|
||||
|
||||
session_id = request.GET.get('session_id')
|
||||
|
|
@ -1017,7 +1031,11 @@ def stream_xc_movie(request, username, password, stream_id, extension):
|
|||
return stream_vod(request._request, 'movie', movie_relation.movie.uuid, session_id, profile_id, user)
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([AllowAny])
|
||||
def stream_xc_episode(request, username, password, stream_id, extension):
|
||||
if not network_access_allowed(request, "STREAMS"):
|
||||
return JsonResponse({"error": "Forbidden"}, status=403)
|
||||
|
||||
from apps.vod.models import M3UEpisodeRelation
|
||||
|
||||
session_id = request.GET.get('session_id')
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from rest_framework import viewsets, status
|
|||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.decorators import api_view, permission_classes, action
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
|
|
@ -35,6 +35,9 @@ import os
|
|||
from core.tasks import rehash_streams
|
||||
from apps.accounts.permissions import (
|
||||
Authenticated,
|
||||
IsAdmin,
|
||||
IsStandardUser,
|
||||
permission_classes_by_action,
|
||||
)
|
||||
from dispatcharr.utils import get_client_ip
|
||||
|
||||
|
|
@ -50,6 +53,12 @@ class UserAgentViewSet(viewsets.ModelViewSet):
|
|||
queryset = UserAgent.objects.all()
|
||||
serializer_class = UserAgentSerializer
|
||||
|
||||
def get_permissions(self):
|
||||
try:
|
||||
return [perm() for perm in permission_classes_by_action[self.action]]
|
||||
except KeyError:
|
||||
return [Authenticated()]
|
||||
|
||||
|
||||
class StreamProfileViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
|
|
@ -59,6 +68,12 @@ class StreamProfileViewSet(viewsets.ModelViewSet):
|
|||
queryset = StreamProfile.objects.all()
|
||||
serializer_class = StreamProfileSerializer
|
||||
|
||||
def get_permissions(self):
|
||||
try:
|
||||
return [perm() for perm in permission_classes_by_action[self.action]]
|
||||
except KeyError:
|
||||
return [Authenticated()]
|
||||
|
||||
|
||||
class CoreSettingsViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
|
|
@ -69,6 +84,12 @@ class CoreSettingsViewSet(viewsets.ModelViewSet):
|
|||
queryset = CoreSettings.objects.all()
|
||||
serializer_class = CoreSettingsSerializer
|
||||
|
||||
def get_permissions(self):
|
||||
try:
|
||||
return [perm() for perm in permission_classes_by_action[self.action]]
|
||||
except KeyError:
|
||||
return [Authenticated()]
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
old_value = instance.value
|
||||
|
|
@ -171,6 +192,11 @@ class ProxySettingsViewSet(viewsets.ViewSet):
|
|||
"""
|
||||
serializer_class = ProxySettingsSerializer
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ('list', 'retrieve'):
|
||||
return [IsStandardUser()]
|
||||
return [IsAdmin()]
|
||||
|
||||
def _get_or_create_settings(self):
|
||||
"""Get or create the proxy settings CoreSettings entry"""
|
||||
try:
|
||||
|
|
@ -330,8 +356,8 @@ def environment(request):
|
|||
@extend_schema(
|
||||
description="Get application version information",
|
||||
)
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([AllowAny])
|
||||
def version(request):
|
||||
# Import version information
|
||||
from version import __version__, __timestamp__
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import logging
|
|||
import time
|
||||
import os
|
||||
import threading
|
||||
from pathlib import Path
|
||||
import re
|
||||
from django.conf import settings
|
||||
from redis.exceptions import ConnectionError, TimeoutError
|
||||
|
|
@ -431,6 +432,20 @@ def cleanup_memory(log_usage=False, force_collection=True):
|
|||
pass
|
||||
logger.trace("Memory cleanup complete for django")
|
||||
|
||||
def safe_upload_path(filename: str, base_dir) -> str:
|
||||
"""Return a safe absolute path for an uploaded file within base_dir.
|
||||
|
||||
Strips all directory components from *filename* and verifies the resolved
|
||||
path stays inside *base_dir*. Raises ValueError on path traversal attempts.
|
||||
"""
|
||||
safe_name = Path(filename).name
|
||||
base = Path(base_dir).resolve()
|
||||
file_path = (base / safe_name).resolve()
|
||||
if not file_path.is_relative_to(base):
|
||||
raise ValueError("Invalid filename.")
|
||||
return str(file_path)
|
||||
|
||||
|
||||
def is_protected_path(file_path):
|
||||
"""
|
||||
Determine if a file path is in a protected directory that shouldn't be deleted.
|
||||
|
|
|
|||
|
|
@ -265,7 +265,14 @@ REST_FRAMEWORK = {
|
|||
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
||||
"apps.accounts.authentication.ApiKeyAuthentication",
|
||||
],
|
||||
"DEFAULT_PERMISSION_CLASSES": [
|
||||
"apps.accounts.permissions.IsAdmin",
|
||||
],
|
||||
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
|
||||
"DEFAULT_THROTTLE_CLASSES": [],
|
||||
"DEFAULT_THROTTLE_RATES": {
|
||||
"login": "3/minute",
|
||||
},
|
||||
}
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
|
|
@ -413,7 +420,6 @@ BACKUP_DATA_DIRS = [
|
|||
SERVER_IP = "127.0.0.1"
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CSRF_TRUSTED_ORIGINS = ["http://*", "https://*"]
|
||||
APPEND_SLASH = True
|
||||
|
||||
|
|
|
|||
18
frontend/package-lock.json
generated
18
frontend/package-lock.json
generated
|
|
@ -2518,9 +2518,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.11",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
|
||||
"integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
|
||||
"version": "0.8.12",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz",
|
||||
"integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
|
@ -3986,9 +3986,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.clamp": {
|
||||
|
|
@ -5490,9 +5490,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
|
||||
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue