From 06704aaa77f56bad8c956d0b93f52fad1746104a Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" Date: Sat, 31 Dec 2022 17:28:15 +0100 Subject: [PATCH] Limit access to files to user No database migration is made --- ...file_alter_invoice_transaction_and_more.py | 4 +-- ...options_alter_category_options_and_more.py | 4 +-- nummi/main/models.py | 34 ++++++++++++------- nummi/main/urls.py | 1 + nummi/main/views.py | 22 +++++++++++- nummi/nummi/urls.py | 4 +-- nummi/nummi/views.py | 14 -------- 7 files changed, 48 insertions(+), 35 deletions(-) delete mode 100644 nummi/nummi/views.py diff --git a/nummi/main/migrations/0009_alter_invoice_file_alter_invoice_transaction_and_more.py b/nummi/main/migrations/0009_alter_invoice_file_alter_invoice_transaction_and_more.py index 934877e..64f47f1 100644 --- a/nummi/main/migrations/0009_alter_invoice_file_alter_invoice_transaction_and_more.py +++ b/nummi/main/migrations/0009_alter_invoice_file_alter_invoice_transaction_and_more.py @@ -19,7 +19,7 @@ class Migration(migrations.Migration): name="file", field=models.FileField( max_length=128, - upload_to=main.models.invoice_path, + upload_to=main.models.get_path, validators=[django.core.validators.FileExtensionValidator(["pdf"])], verbose_name="Fichier", ), @@ -40,7 +40,7 @@ class Migration(migrations.Migration): blank=True, default="", max_length=256, - upload_to=main.models.snapshot_path, + upload_to=main.models.get_path, validators=[django.core.validators.FileExtensionValidator(["pdf"])], verbose_name="Fichier", ), diff --git a/nummi/main/migrations/0021_alter_account_options_alter_category_options_and_more.py b/nummi/main/migrations/0021_alter_account_options_alter_category_options_and_more.py index 4885c6f..ce56899 100644 --- a/nummi/main/migrations/0021_alter_account_options_alter_category_options_and_more.py +++ b/nummi/main/migrations/0021_alter_account_options_alter_category_options_and_more.py @@ -118,7 +118,7 @@ class Migration(migrations.Migration): name="file", field=models.FileField( max_length=128, - upload_to=main.models.invoice_path, + upload_to=main.models.get_path, validators=[django.core.validators.FileExtensionValidator(["pdf"])], verbose_name="File", ), @@ -174,7 +174,7 @@ class Migration(migrations.Migration): blank=True, default="", max_length=256, - upload_to=main.models.snapshot_path, + upload_to=main.models.get_path, validators=[django.core.validators.FileExtensionValidator(["pdf"])], verbose_name="File", ), diff --git a/nummi/main/models.py b/nummi/main/models.py index e17dfe5..5a32d8b 100644 --- a/nummi/main/models.py +++ b/nummi/main/models.py @@ -6,6 +6,7 @@ from django.conf import settings from django.core.validators import FileExtensionValidator, validate_slug from django.db import models, transaction from django.urls import reverse +from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ @@ -30,6 +31,15 @@ class CustomModel(UserModel): abstract = True +def get_path(instance, filename): + return pathlib.Path( + "user", + str(instance.user.get_username()), + instance._meta.model_name, + str(instance.pk), + ).with_suffix(".pdf") + + class Account(CustomModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=64, default=_("Account"), verbose_name=_("Name")) @@ -104,10 +114,6 @@ class Category(CustomModel): verbose_name_plural = _("Categories") -def snapshot_path(instance, filename): - return pathlib.Path("snapshots", str(instance.id)).with_suffix(".pdf") - - class Snapshot(AccountModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) date = models.DateField(default=datetime.date.today, verbose_name=_("End date")) @@ -135,7 +141,7 @@ class Snapshot(AccountModel): editable=False, ) file = models.FileField( - upload_to=snapshot_path, + upload_to=get_path, validators=[FileExtensionValidator(["pdf"])], verbose_name=_("File"), max_length=256, @@ -258,17 +264,13 @@ class Transaction(CustomModel): verbose_name_plural = _("Transactions") -def invoice_path(instance, filename): - return pathlib.Path("invoices", str(instance.id)).with_suffix(".pdf") - - class Invoice(CustomModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField( max_length=256, default=_("Invoice"), verbose_name=_("Name") ) file = models.FileField( - upload_to=invoice_path, + upload_to=get_path, validators=[FileExtensionValidator(["pdf"])], verbose_name=_("File"), max_length=128, @@ -277,13 +279,19 @@ class Invoice(CustomModel): Transaction, on_delete=models.CASCADE, editable=False ) + def save(self): + if Invoice.objects.filter(id=self.id).exists(): + _prever = Invoice.objects.get(id=self.id) + if _prever.file and _prever.file != self.file: + pathlib.Path(_prever.file.path).unlink(missing_ok=True) + def __str__(self): if hasattr(self, "transaction"): - return f"{self.name} – {self.transaction.name}" - return self.name + return str(format_lazy("{} – {}", self.name, self.transaction.name)) + return str(self.name) def delete(self, *args, **kwargs): - self.file.delete() + self.file.delete(missing_ok=True) super().delete(*args, **kwargs) def get_absolute_url(self): diff --git a/nummi/main/urls.py b/nummi/main/urls.py index c823a93..2ea5506 100644 --- a/nummi/main/urls.py +++ b/nummi/main/urls.py @@ -4,6 +4,7 @@ from . import views urlpatterns = [ path("", views.IndexView.as_view(), name="index"), + path("media/user//", views.MediaView.as_view(), name="media"), path("login", views.LoginView.as_view(), name="login"), path("logout", views.LogoutView.as_view(), name="logout"), path("transactions", views.TransactionListView.as_view(), name="transactions"), diff --git a/nummi/main/views.py b/nummi/main/views.py index a979e65..2df7c45 100644 --- a/nummi/main/views.py +++ b/nummi/main/views.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.contrib.auth import views as auth_views from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.postgres.search import ( @@ -6,9 +7,12 @@ from django.contrib.postgres.search import ( SearchVector, TrigramSimilarity, ) +from django.core.exceptions import PermissionDenied from django.db import models +from django.http import HttpResponse from django.shortcuts import get_object_or_404, redirect from django.urls import reverse_lazy +from django.views import View from django.views.generic import ( CreateView, DeleteView, @@ -16,7 +20,7 @@ from django.views.generic import ( TemplateView, UpdateView, ) -from django.views.generic.edit import ProcessFormView +from django.views.static import serve from .forms import AccountForm, CategoryForm, InvoiceForm, SnapshotForm, TransactionForm from .models import Account, Category, Invoice, Snapshot, Transaction @@ -334,3 +338,19 @@ class SearchView(TransactionListView): def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) | {"search": self.kwargs["search"]} + + +class MediaView(View): + def get(self, request, *args, **kwargs): + _username = kwargs.get("username") + _path = kwargs.get("path") + if request.user.get_username() != _username: + raise PermissionDenied + + if settings.DEBUG: + return serve(request, f"user/{_username}/{_path}", settings.MEDIA_ROOT) + + _res = HttpResponse() + _res["Content-Type"] = "" + _res["X-Accel-Redirect"] = f"/internal/media/user/{_username}/{_path}" + return _res diff --git a/nummi/nummi/urls.py b/nummi/nummi/urls.py index 032f4b9..14d3517 100644 --- a/nummi/nummi/urls.py +++ b/nummi/nummi/urls.py @@ -17,9 +17,7 @@ from django.conf.urls.i18n import i18n_patterns from django.contrib import admin from django.urls import include, path -from . import views - -urlpatterns = [path("media/", views.media, name="media"),] + i18n_patterns( +urlpatterns = i18n_patterns( path("", include("main.urls")), path("plot/", include("plot.urls")), path("admin/", admin.site.urls), diff --git a/nummi/nummi/views.py b/nummi/nummi/views.py deleted file mode 100644 index 15ffea3..0000000 --- a/nummi/nummi/views.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.conf import settings -from django.contrib.auth.decorators import login_required -from django.http import HttpResponse -from django.views.static import serve - - -@login_required -def media(request, path): - if settings.DEBUG: - return serve(request, path, settings.MEDIA_ROOT) - _res = HttpResponse() - _res["Content-Type"] = "" - _res["X-Accel-Redirect"] = "/internal/media/" + path - return _res