okay
This commit is contained in:
54
backend/account/migrations/0001_initial.py
Normal file
54
backend/account/migrations/0001_initial.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-28 22:28
|
||||
|
||||
import account.models
|
||||
import django.contrib.auth.validators
|
||||
import django.core.validators
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CustomUser',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('is_deleted', models.BooleanField(default=False)),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
||||
('role', models.CharField(choices=[('admin', 'Admin'), ('mod', 'Moderator'), ('regular', 'Regular')], default='regular', max_length=20)),
|
||||
('phone_number', models.CharField(blank=True, max_length=16, null=True, unique=True, validators=[django.core.validators.RegexValidator('^\\+?\\d{9,15}$', message='Zadejte platné telefonní číslo.')])),
|
||||
('email_verified', models.BooleanField(default=False)),
|
||||
('email', models.EmailField(db_index=True, max_length=254, unique=True)),
|
||||
('gdpr', models.BooleanField(default=False)),
|
||||
('is_active', models.BooleanField(default=False)),
|
||||
('create_time', models.DateTimeField(auto_now_add=True)),
|
||||
('city', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('street', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('postal_code', models.CharField(blank=True, max_length=5, null=True, validators=[django.core.validators.RegexValidator(code='invalid_postal_code', message='Postal code must contain exactly 5 digits.', regex='^\\d{5}$')])),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to.', related_name='customuser_set', related_query_name='customuser', to='auth.group')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='customuser_set', related_query_name='customuser', to='auth.permission')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
managers=[
|
||||
('objects', account.models.CustomUserManager()),
|
||||
('active', account.models.ActiveUserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
import uuid
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser, Group, Permission
|
||||
from django.contrib.auth.models import AbstractUser, UserManager, Group, Permission
|
||||
from django.core.validators import RegexValidator, MinLengthValidator, MaxValueValidator, MinValueValidator
|
||||
|
||||
from django.conf import settings
|
||||
@@ -16,7 +16,13 @@ import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CustomUserManager(UserManager):
|
||||
# Inherit get_by_natural_key and all auth behaviors
|
||||
use_in_migrations = True
|
||||
|
||||
class ActiveUserManager(CustomUserManager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(is_active=True)
|
||||
|
||||
class CustomUser(SoftDeleteModel, AbstractUser):
|
||||
groups = models.ManyToManyField(
|
||||
@@ -83,9 +89,10 @@ class CustomUser(SoftDeleteModel, AbstractUser):
|
||||
"email"
|
||||
]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.email} at {self.create_time.strftime('%d-%m-%Y %H:%M:%S')}"
|
||||
# Ensure default manager has get_by_natural_key
|
||||
objects = CustomUserManager()
|
||||
# Optional convenience manager for active users only
|
||||
active = ActiveUserManager()
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
self.is_active = False
|
||||
@@ -93,25 +100,29 @@ class CustomUser(SoftDeleteModel, AbstractUser):
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.pk is None: # if newely created user
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
group, _ = Group.objects.get_or_create(name=self.role)
|
||||
self.groups.set([group])
|
||||
is_new = self._state.adding # True if object hasn't been saved yet
|
||||
|
||||
# Pre-save flags for new users
|
||||
if is_new:
|
||||
if self.is_superuser or self.role == "admin":
|
||||
# ensure admin flags are consistent
|
||||
self.is_active = True
|
||||
|
||||
if self.role == 'admin':
|
||||
self.is_staff = True
|
||||
self.is_superuser = True
|
||||
|
||||
if self.is_superuser:
|
||||
self.role = 'admin'
|
||||
|
||||
self.is_staff = True
|
||||
self.is_superuser = True
|
||||
self.role = "admin"
|
||||
else:
|
||||
self.is_staff = False
|
||||
|
||||
|
||||
# First save to obtain a primary key
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Assign group after we have a PK
|
||||
if is_new:
|
||||
from django.contrib.auth.models import Group
|
||||
group, _ = Group.objects.get_or_create(name=self.role)
|
||||
# Use add/set now that PK exists
|
||||
self.groups.set([group])
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -27,21 +27,16 @@ class CustomUserSerializer(serializers.ModelSerializer):
|
||||
"last_name",
|
||||
"email",
|
||||
"role",
|
||||
"account_type",
|
||||
"email_verified",
|
||||
"phone_number",
|
||||
"create_time",
|
||||
"var_symbol",
|
||||
"bank_account",
|
||||
"ICO",
|
||||
"RC",
|
||||
"city",
|
||||
"street",
|
||||
"PSC",
|
||||
"GDPR",
|
||||
"postal_code",
|
||||
"gdpr",
|
||||
"is_active",
|
||||
]
|
||||
read_only_fields = ["id", "create_time", "GDPR", "username"] # <-- removed "account_type"
|
||||
read_only_fields = ["id", "create_time", "gdpr", "username"] # <-- removed "account_type"
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
user = self.context["request"].user
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-28 01:24
|
||||
# Generated by Django 5.2.7 on 2025-10-28 22:28
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -78,7 +78,7 @@ django-celery-beat #slouží k plánování úkolů pro Celery
|
||||
#opencv-python #moviepy use this better instead of pillow
|
||||
#moviepy
|
||||
|
||||
#yt-dlp
|
||||
yt-dlp
|
||||
|
||||
weasyprint #tvoření PDFek z html dokumentu + css styly
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-28 00:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DownloaderModel',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('url', models.URLField()),
|
||||
('download_time', models.DateTimeField(auto_now_add=True)),
|
||||
('status', models.CharField(max_length=50)),
|
||||
('requested_format', models.CharField(blank=True, db_index=True, max_length=100, null=True)),
|
||||
('format_id', models.CharField(blank=True, db_index=True, max_length=50, null=True)),
|
||||
('ext', models.CharField(blank=True, db_index=True, max_length=20, null=True)),
|
||||
('vcodec', models.CharField(blank=True, db_index=True, max_length=50, null=True)),
|
||||
('acodec', models.CharField(blank=True, db_index=True, max_length=50, null=True)),
|
||||
('width', models.IntegerField(blank=True, null=True)),
|
||||
('height', models.IntegerField(blank=True, null=True)),
|
||||
('fps', models.FloatField(blank=True, null=True)),
|
||||
('abr', models.FloatField(blank=True, null=True)),
|
||||
('vbr', models.FloatField(blank=True, null=True)),
|
||||
('tbr', models.FloatField(blank=True, null=True)),
|
||||
('asr', models.IntegerField(blank=True, null=True)),
|
||||
('filesize', models.BigIntegerField(blank=True, null=True)),
|
||||
('duration', models.FloatField(blank=True, null=True)),
|
||||
('title', models.CharField(blank=True, max_length=512, null=True)),
|
||||
('extractor', models.CharField(blank=True, db_index=True, max_length=100, null=True)),
|
||||
('extractor_key', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('video_id', models.CharField(blank=True, db_index=True, max_length=128, null=True)),
|
||||
('webpage_url', models.URLField(blank=True, null=True)),
|
||||
('is_audio_only', models.BooleanField(db_index=True, default=False)),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('user_agent', models.TextField(blank=True, null=True)),
|
||||
('error_message', models.TextField(blank=True, null=True)),
|
||||
('raw_info', models.JSONField(blank=True, null=True)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'indexes': [models.Index(fields=['download_time'], name='downloader__downloa_ef522e_idx'), models.Index(fields=['ext', 'is_audio_only'], name='downloader__ext_2aa7af_idx'), models.Index(fields=['requested_format'], name='downloader__request_f4048b_idx'), models.Index(fields=['extractor'], name='downloader__extract_b39777_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
13
backend/thirdparty/downloader/urls.py
vendored
13
backend/thirdparty/downloader/urls.py
vendored
@@ -1,14 +1,13 @@
|
||||
from django.urls import path
|
||||
from .views import DownloaderLogView, DownloaderStatsView
|
||||
from .views import DownloaderFormatsView, DownloaderFileView
|
||||
from .views import DownloaderFormatsView, DownloaderFileView, DownloaderStatsView
|
||||
|
||||
urlpatterns = [
|
||||
# Probe formats for a URL (size-checked)
|
||||
path("api/downloader/formats/", DownloaderFormatsView.as_view(), name="downloader-formats"),
|
||||
path("formats/", DownloaderFormatsView.as_view(), name="downloader-formats"),
|
||||
|
||||
# Download selected format (enforces size limit)
|
||||
path("api/downloader/download/", DownloaderFileView.as_view(), name="downloader-download"),
|
||||
path("download/", DownloaderFileView.as_view(), name="downloader-download"),
|
||||
|
||||
# Aggregated statistics
|
||||
path("api/downloader/stats/", DownloaderStatsView.as_view(), name="downloader-stats"),
|
||||
# Legacy helper
|
||||
path("api/downloader/logs/", DownloaderLogView.as_view(), name="downloader-log"),
|
||||
path("stats/", DownloaderStatsView.as_view(), name="downloader-stats"),
|
||||
]
|
||||
|
||||
62
backend/thirdparty/downloader/views.py
vendored
62
backend/thirdparty/downloader/views.py
vendored
@@ -9,6 +9,7 @@ from django.http import StreamingHttpResponse, JsonResponse
|
||||
from django.utils.text import slugify
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
|
||||
# docs + schema helpers
|
||||
from rest_framework import serializers
|
||||
@@ -26,6 +27,7 @@ import math
|
||||
import json
|
||||
import tempfile
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from urllib.parse import quote as urlquote
|
||||
|
||||
from .models import DownloaderModel
|
||||
from .serializers import DownloaderLogSerializer
|
||||
@@ -141,9 +143,30 @@ def _client_meta(request) -> Tuple[Optional[Any], Optional[str], Optional[str]]:
|
||||
user = getattr(request, "user", None)
|
||||
return user, ip, ua
|
||||
|
||||
# Safe logger: swallow DB errors if table is missing/not migrated yet
|
||||
def _log_safely(*, info, requested_format, status: str, url: str, user, ip_address: str, user_agent: str, error_message: str | None = None):
|
||||
try:
|
||||
DownloaderModel.from_ydl_info(
|
||||
info=info,
|
||||
requested_format=requested_format,
|
||||
status=status,
|
||||
url=url,
|
||||
user=user,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
error_message=error_message,
|
||||
)
|
||||
except (OperationalError, ProgrammingError):
|
||||
# migrations not applied or table missing – ignore
|
||||
pass
|
||||
except Exception:
|
||||
# never break the request on logging failures
|
||||
pass
|
||||
|
||||
class DownloaderFormatsView(APIView):
|
||||
"""Probe media URL and return available formats with estimated sizes and limit flags."""
|
||||
permission_classes = [AllowAny]
|
||||
authentication_classes = []
|
||||
|
||||
@extend_schema(
|
||||
tags=["downloader"],
|
||||
@@ -224,7 +247,7 @@ class DownloaderFormatsView(APIView):
|
||||
except Exception as e:
|
||||
# log probe error
|
||||
user, ip, ua = _client_meta(request)
|
||||
DownloaderModel.from_ydl_info(
|
||||
_log_safely(
|
||||
info={"webpage_url": url},
|
||||
requested_format=None,
|
||||
status="probe_error",
|
||||
@@ -258,7 +281,7 @@ class DownloaderFormatsView(APIView):
|
||||
|
||||
# Log probe
|
||||
user, ip, ua = _client_meta(request)
|
||||
DownloaderModel.from_ydl_info(
|
||||
_log_safely(
|
||||
info=info,
|
||||
requested_format=None,
|
||||
status="probe_ok",
|
||||
@@ -280,6 +303,7 @@ class DownloaderFormatsView(APIView):
|
||||
class DownloaderFileView(APIView):
|
||||
"""Download selected format if under max size, then stream the file back."""
|
||||
permission_classes = [AllowAny]
|
||||
authentication_classes = []
|
||||
|
||||
@extend_schema(
|
||||
tags=["downloader"],
|
||||
@@ -288,7 +312,7 @@ class DownloaderFileView(APIView):
|
||||
description="Downloads with a strict max filesize guard and streams as application/octet-stream.",
|
||||
request=DownloadRequestSchema,
|
||||
responses={
|
||||
200: OpenApiResponse(response=OpenApiTypes.BINARY, media_type="application/octet-stream"),
|
||||
200: OpenApiTypes.BINARY, # was OpenApiResponse(..., media_type="application/octet-stream")
|
||||
400: OpenApiResponse(response=ErrorResponseSchema),
|
||||
413: OpenApiResponse(response=ErrorResponseSchema),
|
||||
500: OpenApiResponse(response=ErrorResponseSchema),
|
||||
@@ -334,7 +358,7 @@ class DownloaderFileView(APIView):
|
||||
info = ydl.extract_info(url, download=False)
|
||||
except Exception as e:
|
||||
user, ip, ua = _client_meta(request)
|
||||
DownloaderModel.from_ydl_info(
|
||||
_log_safely(
|
||||
info={"webpage_url": url},
|
||||
requested_format=fmt_id,
|
||||
status="precheck_error",
|
||||
@@ -358,7 +382,7 @@ class DownloaderFileView(APIView):
|
||||
est_size = _estimate_size_bytes(selected, duration)
|
||||
if est_size is not None and est_size > max_bytes:
|
||||
user, ip, ua = _client_meta(request)
|
||||
DownloaderModel.from_ydl_info(
|
||||
_log_safely(
|
||||
info=selected,
|
||||
requested_format=fmt_id,
|
||||
status="blocked_by_size",
|
||||
@@ -400,7 +424,7 @@ class DownloaderFileView(APIView):
|
||||
filepath = result.get("requested_downloads", [{}])[0].get("filepath") or result.get("_filename")
|
||||
except Exception as e:
|
||||
user, ip, ua = _client_meta(request)
|
||||
DownloaderModel.from_ydl_info(
|
||||
_log_safely(
|
||||
info=selected,
|
||||
requested_format=fmt_id,
|
||||
status="download_error",
|
||||
@@ -425,7 +449,7 @@ class DownloaderFileView(APIView):
|
||||
try:
|
||||
selected_info = dict(selected)
|
||||
selected_info["filesize"] = os.path.getsize(filepath)
|
||||
DownloaderModel.from_ydl_info(
|
||||
_log_safely(
|
||||
info=selected_info,
|
||||
requested_format=fmt_id,
|
||||
status="success",
|
||||
@@ -451,7 +475,13 @@ class DownloaderFileView(APIView):
|
||||
pass
|
||||
|
||||
resp = StreamingHttpResponse(file_generator(filepath), content_type="application/octet-stream")
|
||||
resp["Content-Disposition"] = f'attachment; filename="{safe_name}"'
|
||||
# Include both plain and RFC 5987 encoded filename
|
||||
resp["Content-Disposition"] = (
|
||||
f'attachment; filename="{safe_name}"; filename*=UTF-8\'\'{urlquote(safe_name)}'
|
||||
)
|
||||
# Expose headers so the browser can read them via XHR/fetch
|
||||
resp["X-Filename"] = safe_name
|
||||
resp["Access-Control-Expose-Headers"] = "Content-Disposition, X-Filename, Content-Length, Content-Type"
|
||||
try:
|
||||
resp["Content-Length"] = str(os.path.getsize(filepath))
|
||||
except Exception:
|
||||
@@ -521,19 +551,3 @@ class DownloaderStatsView(APIView):
|
||||
"top_acodec": top_acodec,
|
||||
"audio_vs_video": audio_vs_video,
|
||||
})
|
||||
|
||||
|
||||
# Minimal placeholder so existing URL doesn't break; prefer using automatic logs above.
|
||||
class DownloaderLogView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
tags=["downloader"],
|
||||
operation_id="downloader_log_helper",
|
||||
summary="Deprecated helper",
|
||||
description="Use /api/downloader/formats/ then /api/downloader/download/.",
|
||||
responses={200: inline_serializer(name="LogHelper", fields={"detail": serializers.CharField()})},
|
||||
)
|
||||
def post(self, request):
|
||||
"""POST to the deprecated log helper endpoint."""
|
||||
return Response({"detail": "Use /api/downloader/formats/ then /api/downloader/download/."}, status=200)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-28 00:13
|
||||
# Generated by Django 5.2.7 on 2025-10-28 22:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trznice.settings')
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
"http": get_asgi_application(),
|
||||
"websocket": AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
#myapp.routing.websocket_urlpatterns
|
||||
)
|
||||
),
|
||||
# "websocket": AuthMiddlewareStack(
|
||||
# URLRouter(
|
||||
# #myapp.routing.websocket_urlpatterns
|
||||
# )
|
||||
# ),
|
||||
})
|
||||
|
||||
@@ -32,8 +32,11 @@ urlpatterns = [
|
||||
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/account/', include('account.urls')),
|
||||
#path('api/commerce/', include('commerce.urls')),
|
||||
#path('api/advertisments/', include('advertisements.urls')),
|
||||
|
||||
path('api/stripe/', include('thirdparty.stripe.urls')),
|
||||
path('api/trading212/', include('thirdparty.trading212.urls')),
|
||||
path('api/downloader/', include('thirdparty.downloader.urls')),
|
||||
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user