Compare commits
2 Commits
7d9b2bb88e
...
11199adc38
| Author | SHA1 | Date | |
|---|---|---|---|
| 11199adc38 | |||
| 553ac9abf1 |
@@ -18,9 +18,11 @@ import secrets
|
|||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import IntegrityError, transaction
|
from django.db import IntegrityError, transaction
|
||||||
|
from django.http import FileResponse, Http404
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
|
from django.views.decorators.http import require_safe
|
||||||
from django.views.generic import CreateView
|
from django.views.generic import CreateView
|
||||||
|
|
||||||
from .emails import send_confirmation_email
|
from .emails import send_confirmation_email
|
||||||
@@ -28,6 +30,48 @@ from .forms import SubmissionForm
|
|||||||
from .models import Submission, VerifiedEmail
|
from .models import Submission, VerifiedEmail
|
||||||
|
|
||||||
|
|
||||||
|
@require_safe
|
||||||
|
def serve_stl(request, path: str):
|
||||||
|
"""Auth-checked passthrough to a Submission's uploaded `.stl`. URL is
|
||||||
|
`/media/<path>` so it matches `MEDIA_URL` -- which means
|
||||||
|
`{{ submission.stl_file.url }}` in templates Just Works.
|
||||||
|
|
||||||
|
`path` is the relative name Django stored (e.g. `stl/foo.stl`); we
|
||||||
|
look the row up by that string rather than reading the filesystem
|
||||||
|
directly so an attacker can't escape `MEDIA_ROOT` with `..` segments.
|
||||||
|
|
||||||
|
Access tiers match `SubmissionDetailView` (plan.md §7.4 step 7):
|
||||||
|
|
||||||
|
- staff: yes (operators need the file to drive the printer).
|
||||||
|
- authenticated owner (`submitted_by == request.user`): yes.
|
||||||
|
- everyone else: 404 -- we deliberately don't 403 so the existence
|
||||||
|
of a row isn't leaked to non-owners by status code.
|
||||||
|
|
||||||
|
Guests can't authenticate by design, so they can't pull their own STL
|
||||||
|
via this path. That's consistent with the detail page's two-tier
|
||||||
|
access; the submitter has the file on their own machine in any case.
|
||||||
|
"""
|
||||||
|
sub = Submission.objects.filter(stl_file=path).first()
|
||||||
|
if sub is None:
|
||||||
|
raise Http404
|
||||||
|
user = request.user
|
||||||
|
is_owner = (
|
||||||
|
user.is_authenticated
|
||||||
|
and sub.submitted_by_id is not None
|
||||||
|
and sub.submitted_by_id == user.id
|
||||||
|
)
|
||||||
|
if not (user.is_staff or is_owner):
|
||||||
|
raise Http404
|
||||||
|
# `FieldFile.open("rb")` is rooted in MEDIA_ROOT; FileResponse handles
|
||||||
|
# streaming, range requests, and closing the handle for us.
|
||||||
|
filename = sub.stl_file.name.rsplit("/", 1)[-1]
|
||||||
|
return FileResponse(
|
||||||
|
sub.stl_file.open("rb"),
|
||||||
|
as_attachment=True,
|
||||||
|
filename=filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SubmitView(CreateView):
|
class SubmitView(CreateView):
|
||||||
"""Public submit form. GET renders, POST creates a Submission."""
|
"""Public submit form. GET renders, POST creates a Submission."""
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ STATIC_URL = "static/"
|
|||||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||||
STATICFILES_DIRS = [BASE_DIR / "static"] if (BASE_DIR / "static").exists() else []
|
STATICFILES_DIRS = [BASE_DIR / "static"] if (BASE_DIR / "static").exists() else []
|
||||||
|
|
||||||
MEDIA_URL = "media/"
|
MEDIA_URL = "/media/"
|
||||||
MEDIA_ROOT = BASE_DIR / "media"
|
MEDIA_ROOT = BASE_DIR / "media"
|
||||||
|
|
||||||
STORAGES = {
|
STORAGES = {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ Including another URLconf
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
|
from apps.submissions.views import serve_stl
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
# Our local /accounts/ routes (close-account, etc.) come BEFORE allauth's
|
# Our local /accounts/ routes (close-account, etc.) come BEFORE allauth's
|
||||||
@@ -25,6 +27,10 @@ urlpatterns = [
|
|||||||
# through to allauth.
|
# through to allauth.
|
||||||
path("accounts/", include("apps.accounts.urls")),
|
path("accounts/", include("apps.accounts.urls")),
|
||||||
path("accounts/", include("allauth.urls")),
|
path("accounts/", include("allauth.urls")),
|
||||||
|
# Auth-checked media serve. Lives here (not in apps.submissions.urls)
|
||||||
|
# because the prefix is MEDIA_URL, not /submit/. WhiteNoise serves
|
||||||
|
# /static/ only, so this is the sole handler for /media/.
|
||||||
|
path("media/<path:path>", serve_stl, name="serve_stl"),
|
||||||
path("", include("apps.dashboard.urls")),
|
path("", include("apps.dashboard.urls")),
|
||||||
path("submit/", include("apps.submissions.urls")),
|
path("submit/", include("apps.submissions.urls")),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -136,13 +136,24 @@
|
|||||||
<div class="bg-white border border-slate-200 rounded-lg p-5">
|
<div class="bg-white border border-slate-200 rounded-lg p-5">
|
||||||
<h3 class="font-semibold text-sm uppercase tracking-wide text-slate-500 mb-3">Source</h3>
|
<h3 class="font-semibold text-sm uppercase tracking-wide text-slate-500 mb-3">Source</h3>
|
||||||
{% if submission.source_type == 'upload' %}
|
{% if submission.source_type == 'upload' %}
|
||||||
<div class="flex items-center gap-3 p-3 rounded-md bg-slate-50 border border-slate-200">
|
{% if submission.stl_file %}
|
||||||
<svg class="w-8 h-8 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
|
<a href="{{ submission.stl_file.url }}" class="flex items-center gap-3 p-3 rounded-md bg-slate-50 border border-slate-200 hover:border-amber-400 hover:bg-amber-50/40">
|
||||||
<div class="flex-1 min-w-0">
|
<svg class="w-8 h-8 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
|
||||||
<p class="font-medium text-sm break-all">{% if submission.stl_file %}{{ submission.stl_file.name }}{% else %}(file not on disk){% endif %}</p>
|
<div class="flex-1 min-w-0">
|
||||||
<p class="text-xs text-slate-500">Raw <span class="mono">.stl</span> upload</p>
|
<p class="font-medium text-sm break-all">{{ submission.stl_file.name }}</p>
|
||||||
|
<p class="text-xs text-slate-500">Raw <span class="mono">.stl</span> upload · click to download</p>
|
||||||
|
</div>
|
||||||
|
<svg class="w-4 h-4 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<div class="flex items-center gap-3 p-3 rounded-md bg-slate-50 border border-slate-200">
|
||||||
|
<svg class="w-8 h-8 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="font-medium text-sm break-all">(file not on disk)</p>
|
||||||
|
<p class="text-xs text-slate-500">Raw <span class="mono">.stl</span> upload</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ submission.source_url }}" target="_blank" rel="noopener" class="flex items-center gap-3 p-3 rounded-md bg-slate-50 border border-slate-200 hover:border-amber-400 hover:bg-amber-50/40">
|
<a href="{{ submission.source_url }}" target="_blank" rel="noopener" class="flex items-center gap-3 p-3 rounded-md bg-slate-50 border border-slate-200 hover:border-amber-400 hover:bg-amber-50/40">
|
||||||
<span class="w-10 h-10 rounded-md bg-amber-100 grid place-items-center text-amber-700 font-bold text-xs flex-shrink-0">{{ submission.source_type|slice:":1"|upper }}</span>
|
<span class="w-10 h-10 rounded-md bg-amber-100 grid place-items-center text-amber-700 font-bold text-xs flex-shrink-0">{{ submission.source_type|slice:":1"|upper }}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user