Compare commits

...

7 Commits

10 changed files with 151 additions and 10 deletions

43
docs/nginx/nginx.conf Normal file
View File

@@ -0,0 +1,43 @@
server {
listen [::]:443 ssl;
listen 443 ssl;
server_name myhost;
# ssl_certificate ...;
add_header X-Xss-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=15768000";
add_header Permissions-Policy "geolocation=(),midi=(),sync-xhr=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=()";
add_header Content-Security-Policy "child-src 'none'; object-src 'none'";
client_max_body_size 250M;
error_page 403 404 https://$server_name/404;
location / {
proxy_pass http://127.0.0.1:8000;
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_redirect http:// https://;
proxy_connect_timeout 1800;
proxy_read_timeout 1800;
proxy_max_temp_file_size 8192m;
}
# static files
location /static {
root /myroot/ram/storage;
}
# media files
location ~ ^/media/(images|uploads) {
root /myroot/ram/storage;
}
# protected filed to be served via X-Accel-Redirect
location /private {
internal;
alias /myroot/ram/storage/media;
}
}

View File

@@ -56,6 +56,15 @@ class Consist(BaseModel):
order=models.Max("order"), order=models.Max("order"),
).order_by("order") ).order_by("order")
def get_cover(self):
if self.image:
return self.image
else:
consist_item = self.consist_item.first()
if consist_item and consist_item.rolling_stock.image.exists():
return consist_item.rolling_stock.image.first().image
return None
@property @property
def country(self): def country(self):
return self.company.country return self.company.country

View File

@@ -1,12 +1,13 @@
{% load static %}
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
<a href="{{ d.get_absolute_url }}"> <a href="{{ d.get_absolute_url }}">
{% if d.image %} {% if d.get_cover %}
<img class="card-img-top" src="{{ d.image.url }}" alt="{{ d }}"> <img class="card-img-top" src="{{ d.get_cover.url }}" alt="{{ d }}">
{% else %} {% else %}
{% with d.consist_item.first.rolling_stock as r %} <!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
<img class="card-img-top" src="{{ r.image.first.image.url }}" alt="{{ d }}"> <a href="{{d.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}"></a>
{% endwith %}
{% endif %} {% endif %}
</a> </a>
<div class="card-body"> <div class="card-body">

View File

@@ -34,6 +34,7 @@
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
</div> </div>
{% if loads %}
<div class="accordion shadow-sm mt-4" id="accordionLoads"> <div class="accordion shadow-sm mt-4" id="accordionLoads">
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <h2 class="accordion-header">
@@ -52,6 +53,7 @@
</div> </div>
</div> </div>
</div> </div>
{% endif %}
{% endblock %} {% endblock %}
{% block pagination %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}

View File

@@ -6,6 +6,7 @@ from urllib.parse import unquote
from django.conf import settings from django.conf import settings
from django.views import View from django.views import View
from django.urls import Resolver404
from django.http import Http404, HttpResponseBadRequest from django.http import Http404, HttpResponseBadRequest
from django.db.utils import OperationalError, ProgrammingError from django.db.utils import OperationalError, ProgrammingError
from django.db.models import F, Q, Count from django.db.models import F, Q, Count
@@ -63,7 +64,18 @@ def get_items_ordering(config="items_ordering"):
class Render404(View): class Render404(View):
def get(self, request, exception): def get(self, request, exception):
return render(request, "base.html", {"title": "404 page not found"}) generic_message = "Page not found"
if isinstance(exception, Resolver404):
message = generic_message
else:
message = str(exception) if exception else generic_message
return render(
request,
"base.html",
{"title": message},
status=404,
)
class GetData(View): class GetData(View):

View File

@@ -1,4 +1,4 @@
from ram.utils import git_suffix from ram.utils import git_suffix
__version__ = "0.19.6" __version__ = "0.19.8"
__version__ += git_suffix(__file__) __version__ += git_suffix(__file__)

View File

@@ -34,3 +34,4 @@ ALLOWED_HOSTS = ["127.0.0.1", "myhost"]
CSRF_TRUSTED_ORIGINS = ["https://myhost"] CSRF_TRUSTED_ORIGINS = ["https://myhost"]
STATIC_URL = "static/" STATIC_URL = "static/"
MEDIA_URL = "media/" MEDIA_URL = "media/"
USE_X_ACCEL_REDIRECT = True

View File

@@ -206,6 +206,19 @@ ROLLING_STOCK_TYPES = [
FEATURED_ITEMS_MAX = 6 FEATURED_ITEMS_MAX = 6
# If True, use X-Accel-Redirect (Nginx)
# when using X-Accel-Redirect, we don't serve the file
# directly from Django, but let Nginx handle it
# in Nginx config, we need to map /private/ to
# the actual media files location with internal directive
# eg:
# location /private {
# internal;
# alias /path/to/media;
# }
# make also sure that the entire /media is _not_ mapped directly in Nginx
USE_X_ACCEL_REDIRECT = False
try: try:
from ram.local_settings import * from ram.local_settings import *
except ImportError: except ImportError:

View File

@@ -21,17 +21,22 @@ from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from ram.views import UploadImage from ram.views import UploadImage, DownloadFile
from portal.views import Render404 from portal.views import Render404
handler404 = Render404.as_view() handler404 = Render404.as_view()
urlpatterns = [ urlpatterns = [
path("", lambda r: redirect("portal/")), path("", lambda r: redirect("portal/")),
path("admin/", admin.site.urls),
path("tinymce/", include("tinymce.urls")), path("tinymce/", include("tinymce.urls")),
path("tinymce/upload_image", UploadImage.as_view(), name="upload_image"), path("tinymce/upload_image", UploadImage.as_view(), name="upload_image"),
path(
"media/files/<path:filename>",
DownloadFile.as_view(),
name="download_file",
),
path("portal/", include("portal.urls")), path("portal/", include("portal.urls")),
path("admin/", admin.site.urls),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Enable the "/dcc" routing only if the "driver" app is active # Enable the "/dcc" routing only if the "driver" app is active
@@ -55,6 +60,7 @@ if settings.DEBUG:
if settings.REST_ENABLED: if settings.REST_ENABLED:
from django.views.generic import TemplateView from django.views.generic import TemplateView
from rest_framework.schemas import get_schema_view from rest_framework.schemas import get_schema_view
urlpatterns += [ urlpatterns += [
path( path(
"swagger/", "swagger/",

View File

@@ -5,19 +5,26 @@ import posixpath
from pathlib import Path from pathlib import Path
from PIL import Image, UnidentifiedImageError from PIL import Image, UnidentifiedImageError
from django.views import View from django.apps import apps
from django.conf import settings from django.conf import settings
from django.http import ( from django.http import (
Http404,
HttpResponse,
HttpResponseBadRequest, HttpResponseBadRequest,
HttpResponseForbidden, HttpResponseForbidden,
FileResponse,
JsonResponse, JsonResponse,
) )
from django.views import View
from django.utils.text import slugify as slugify from django.utils.text import slugify as slugify
from django.utils.encoding import smart_str
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from ram.models import PrivateDocument
class CustomLimitOffsetPagination(LimitOffsetPagination): class CustomLimitOffsetPagination(LimitOffsetPagination):
default_limit = 10 default_limit = 10
@@ -67,3 +74,50 @@ class UploadImage(View):
), ),
} }
) )
class DownloadFile(View):
def get(self, request, filename, disposition="inline"):
# Clean up the filename to prevent directory traversal attacks
filename = os.path.basename(filename)
# Find a document where the stored file name matches
# Find all models inheriting from PublishableFile
for model in apps.get_models():
if issubclass(model, PrivateDocument) and not model._meta.abstract:
try:
doc = model.objects.get(file__endswith=filename)
if doc.private and not request.user.is_staff:
break
file = doc.file
if not os.path.exists(file.path):
break
# in Nginx config, we need to map /private/ to
# the actual media files location with internal directive
# eg:
# location /private {
# internal;
# alias /path/to/media;
# }
if getattr(settings, "USE_X_ACCEL_REDIRECT", False):
response = HttpResponse()
response["Content-Type"] = ""
response["X-Accel-Redirect"] = f"/private/{file.name}"
else:
response = FileResponse(
open(file.path, "rb"), as_attachment=True
)
response["Content-Disposition"] = (
'{}; filename="{}"'.format(
disposition,
smart_str(os.path.basename(file.path))
)
)
return response
except model.DoesNotExist:
continue
raise Http404("File not found")