diff --git a/README.md b/README.md index 58d8206..5878775 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,6 @@ run and to maintain. ```bash sudo apt install git gettext python3-django python3-django-allauth python3-django-crispy-forms python3-docutils python3-exifread python3-pil - - # Only for production - sudo apt install nginx uwsgi uwsgi-plugin-python3 python3-certbot-nginx ``` 2. **Cloning.** @@ -33,19 +30,8 @@ run and to maintain. 3. **Configuration (production only).** ```bash - # Only for production sudo mkdir static media - sudo cp docs/maintenance.html static/maintenance.html - sudo chown www-data:www-data -R static media - sudo chmod g+rwx -R static media sudo chmod +x maintenance_tool.sh - sudo cp docs/uwsgi_photos.ini /etc/uwsgi/apps-available/uwsgi_photos.ini - sudo ln -s /etc/uwsgi/apps-available/uwsgi_photos.ini /etc/uwsgi/apps-enabled/ - sudo cp docs/nginx_photos_maintenance /etc/nginx/sites-available/photos.crans.org - sudo ln -s /etc/nginx/sites-available/photos.crans.org /etc/nginx/sites-enabled/ - sudo cp docs/letsencrypt_photos.crans.org /etc/letsencrypt/conf.d/photos.crans.org - sudo cp docs/renewal-hooks_post_nginx /etc/letsencrypt/renewal-hooks/post/nginx - sudo certbot --config /etc/letsencrypt/conf.d/photos.crans.org.ini certonly ``` 4. **Database (production only).** @@ -82,7 +68,6 @@ run and to maintain. 6. *Enjoy \o/* - In production, the NGINX site should now work. In development, you can launch the development server using: ```bash diff --git a/photo21/settings.py b/photo21/settings.py index 2436141..88390ae 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -46,6 +46,9 @@ ADMINS = [tuple(a.split(":")) for a in config("ADMINS", default="", cast=Csv()) SESSION_COOKIE_SECURE = not DEBUG CSRF_COOKIE_SECURE = not DEBUG +# Trust Caddy's forwarded proto header +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + # Remember HTTPS for 1 year SECURE_HSTS_SECONDS = 31536000 SECURE_HSTS_INCLUDE_SUBDOMAINS = True @@ -77,6 +80,7 @@ if DEBUG: MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -189,6 +193,18 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static/") MEDIA_ROOT = os.path.join(BASE_DIR, "media") MEDIA_URL = "/media/" +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": "photo21.storage.CompressedManifestStorage", + }, +} + + +WHITENOISE_MANIFEST_STRICT = False + LOCALE_PATHS = [os.path.join(BASE_DIR, "photo21/locale")] FIXTURE_DIRS = [os.path.join(BASE_DIR, "photo21/fixtures")] diff --git a/photo21/storage.py b/photo21/storage.py new file mode 100644 index 0000000..fa1b779 --- /dev/null +++ b/photo21/storage.py @@ -0,0 +1,12 @@ +from whitenoise.storage import CompressedManifestStaticFilesStorage + + +class CompressedManifestStorage(CompressedManifestStaticFilesStorage): + """Like CompressedManifestStaticFilesStorage but silently skips missing + referenced files (e.g. source maps not included in the package).""" + + def hashed_name(self, name, content=None, filename=None): + try: + return super().hashed_name(name, content, filename) + except ValueError: + return name diff --git a/photo21/views.py b/photo21/views.py index 8245c4f..434be5b 100644 --- a/photo21/views.py +++ b/photo21/views.py @@ -2,21 +2,29 @@ # Copyright (C) 2021-2022 Amicale des élèves de l'ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +import os + +from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import HttpResponse +from django.http import FileResponse, Http404 from django.views.generic import ListView, View from photologue.models import Gallery - -class MediaAccess(LoginRequiredMixin, View): +class MediaAccess(View): def get(self, request, path): - response = HttpResponse() - # Content-type will be detected by nginx - del response["Content-Type"] - response["X-Accel-Redirect"] = "/protected/media/" + path - response["Cache-Control"] = 'max-age=2678400' + if not request.user.is_authenticated and not request.session.get('public_gallery_access'): + from django.contrib.auth.views import redirect_to_login + return redirect_to_login(request.get_full_path()) + media_root = os.path.realpath(settings.MEDIA_ROOT) + file_path = os.path.realpath(os.path.join(media_root, path)) + if not file_path.startswith(media_root + os.sep): + raise Http404 + if not os.path.isfile(file_path): + raise Http404 + response = FileResponse(open(file_path, 'rb')) + response['Cache-Control'] = 'max-age=2678400' return response diff --git a/photologue/views.py b/photologue/views.py index c44b1f8..a523fa1 100644 --- a/photologue/views.py +++ b/photologue/views.py @@ -237,6 +237,7 @@ class GalleryPublicView(DetailView): if request.user.is_authenticated: gallery = self.get_object() return redirect("photologue:pl-gallery", slug=gallery.slug) + request.session['public_gallery_access'] = True request.guest_mode = True return super().get(request, *args, **kwargs) diff --git a/requirements.txt b/requirements.txt index 3f38e2d..31eeff0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ ExifRead>=2.1.2 Pillow>=6.0.0 django-debug-toolbar>=3.2.0 python-decouple>=3.6 +whitenoise>=6.0