mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-19 12:59:36 +02:00
feat: implement protected media serving and permission checks for adventure images
This commit is contained in:
parent
f10e171a8e
commit
433599dc20
7 changed files with 130 additions and 55 deletions
|
@ -19,7 +19,7 @@ http {
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80; # NGINX listens on port 80 inside the container
|
listen 80;
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
@ -34,8 +34,12 @@ http {
|
||||||
alias /code/staticfiles/; # Serve static files directly
|
alias /code/staticfiles/; # Serve static files directly
|
||||||
}
|
}
|
||||||
|
|
||||||
location /media/ {
|
# Serve protected media files with X-Accel-Redirect
|
||||||
alias /code/media/; # Serve media files directly
|
location /protectedMedia/ {
|
||||||
|
internal; # Only internal requests are allowed
|
||||||
|
alias /code/media/; # This should match Django MEDIA_ROOT
|
||||||
|
try_files $uri =404; # Return a 404 if the file doesn't exist
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
from adventures.models import AdventureImage
|
||||||
|
|
||||||
|
def checkAdventureImagePermission(imageId, user):
|
||||||
|
"""
|
||||||
|
Checks if the given user has permission to access the specified adventure image.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
imageId (str): The ID of the image to check permissions for.
|
||||||
|
user (User): The user object to check permissions against.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the user has permission to access the image, False otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AdventureImage.DoesNotExist: If the image with the specified ID does not exist.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Construct the full relative path to match the database field
|
||||||
|
image_path = f"images/{imageId}"
|
||||||
|
# Fetch the AdventureImage object
|
||||||
|
adventure = AdventureImage.objects.get(image=image_path).adventure
|
||||||
|
if adventure.is_public:
|
||||||
|
return True
|
||||||
|
elif adventure.user_id == user:
|
||||||
|
return True
|
||||||
|
elif adventure.collection:
|
||||||
|
if adventure.collection.shared_with.filter(id=user.id).exists():
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
except AdventureImage.DoesNotExist:
|
||||||
|
print('No image')
|
||||||
|
return False
|
|
@ -157,7 +157,7 @@ STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = '/media/'
|
||||||
MEDIA_ROOT = BASE_DIR / 'media'
|
MEDIA_ROOT = BASE_DIR / 'media' # This path must match the NGINX root
|
||||||
STATICFILES_DIRS = [BASE_DIR / 'static']
|
STATICFILES_DIRS = [BASE_DIR / 'static']
|
||||||
|
|
||||||
STORAGES = {
|
STORAGES = {
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
from django.urls import include, re_path, path
|
from django.urls import include, re_path, path
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.views.generic import RedirectView, TemplateView
|
from django.views.generic import RedirectView, TemplateView
|
||||||
from django.conf import settings
|
|
||||||
from django.conf.urls.static import static
|
|
||||||
from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView, EnabledSocialProvidersView
|
from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView, EnabledSocialProvidersView
|
||||||
from .views import get_csrf_token, get_public_url
|
from .views import get_csrf_token, get_public_url, serve_protected_media
|
||||||
from drf_yasg.views import get_schema_view
|
from drf_yasg.views import get_schema_view
|
||||||
|
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
|
|
||||||
schema_view = get_schema_view(
|
schema_view = get_schema_view(
|
||||||
|
@ -20,6 +17,9 @@ urlpatterns = [
|
||||||
path('api/', include('worldtravel.urls')),
|
path('api/', include('worldtravel.urls')),
|
||||||
path("_allauth/", include("allauth.headless.urls")),
|
path("_allauth/", include("allauth.headless.urls")),
|
||||||
|
|
||||||
|
# Serve protected media files
|
||||||
|
re_path(r'^media/(?P<path>.*)$', serve_protected_media, name='serve-protected-media'),
|
||||||
|
|
||||||
path('auth/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'),
|
path('auth/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'),
|
||||||
path('auth/users/', PublicUserListView.as_view(), name='public-user-list'),
|
path('auth/users/', PublicUserListView.as_view(), name='public-user-list'),
|
||||||
path('auth/user/<uuid:user_id>/', PublicUserDetailView.as_view(), name='public-user-detail'),
|
path('auth/user/<uuid:user_id>/', PublicUserDetailView.as_view(), name='public-user-detail'),
|
||||||
|
@ -45,5 +45,4 @@ urlpatterns = [
|
||||||
path("api/integrations/", include("integrations.urls")),
|
path("api/integrations/", include("integrations.urls")),
|
||||||
|
|
||||||
# Include the API endpoints:
|
# Include the API endpoints:
|
||||||
|
]
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
|
|
@ -1,6 +1,11 @@
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.middleware.csrf import get_token
|
from django.middleware.csrf import get_token
|
||||||
from os import getenv
|
from os import getenv
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponse, HttpResponseForbidden
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.views.static import serve
|
||||||
|
from adventures.utils.check_adventure_image_permisison import checkAdventureImagePermission
|
||||||
|
|
||||||
def get_csrf_token(request):
|
def get_csrf_token(request):
|
||||||
csrf_token = get_token(request)
|
csrf_token = get_token(request)
|
||||||
|
@ -8,3 +13,26 @@ def get_csrf_token(request):
|
||||||
|
|
||||||
def get_public_url(request):
|
def get_public_url(request):
|
||||||
return JsonResponse({'PUBLIC_URL': getenv('PUBLIC_URL')})
|
return JsonResponse({'PUBLIC_URL': getenv('PUBLIC_URL')})
|
||||||
|
|
||||||
|
def serve_protected_media(request, path):
|
||||||
|
if path.startswith('images/'):
|
||||||
|
image_id = path.split('/')[1]
|
||||||
|
user = request.user
|
||||||
|
if checkAdventureImagePermission(image_id, user):
|
||||||
|
if settings.DEBUG:
|
||||||
|
# In debug mode, serve the file directly
|
||||||
|
return serve(request, path, document_root=settings.MEDIA_ROOT)
|
||||||
|
else:
|
||||||
|
# In production, use X-Accel-Redirect
|
||||||
|
response = HttpResponse()
|
||||||
|
response['Content-Type'] = ''
|
||||||
|
response['X-Accel-Redirect'] = '/protectedMedia/' + path
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
else:
|
||||||
|
response = HttpResponse()
|
||||||
|
response['Content-Type'] = ''
|
||||||
|
response['X-Accel-Redirect'] = '/protectedMedia/' + path
|
||||||
|
return response
|
||||||
|
|
|
@ -120,27 +120,34 @@
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form class="flex gap-2">
|
{#if data.user}
|
||||||
<label class="input input-bordered flex items-center gap-2">
|
<form class="flex gap-2">
|
||||||
<input type="text" bind:value={query} class="grow" placeholder={$t('navbar.search')} />
|
<label class="input input-bordered flex items-center gap-2">
|
||||||
|
<input
|
||||||
<svg
|
type="text"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
bind:value={query}
|
||||||
viewBox="0 0 16 16"
|
class="grow"
|
||||||
fill="currentColor"
|
placeholder={$t('navbar.search')}
|
||||||
class="h-4 w-4 opacity-70"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
|
||||||
</label>
|
<svg
|
||||||
<button on:click={searchGo} type="submit" class="btn btn-primary"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>{$t('navbar.search')}</button
|
viewBox="0 0 16 16"
|
||||||
>
|
fill="currentColor"
|
||||||
</form>
|
class="h-4 w-4 opacity-70"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
<button on:click={searchGo} type="submit" class="btn btn-primary"
|
||||||
|
>{$t('navbar.search')}</button
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-ghost text-xl" href="/"
|
<a class="btn btn-ghost text-xl" href="/"
|
||||||
|
@ -190,27 +197,29 @@
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form class="flex gap-2">
|
{#if data.user}
|
||||||
<label class="input input-bordered flex items-center gap-2">
|
<form class="flex gap-2">
|
||||||
<input type="text" bind:value={query} class="grow" placeholder={$t('navbar.search')} />
|
<label class="input input-bordered flex items-center gap-2">
|
||||||
|
<input type="text" bind:value={query} class="grow" placeholder={$t('navbar.search')} />
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="h-4 w-4 opacity-70"
|
class="h-4 w-4 opacity-70"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
<button on:click={searchGo} type="submit" class="btn btn-neutral"
|
||||||
|
>{$t('navbar.search')}</button
|
||||||
>
|
>
|
||||||
<path
|
</form>
|
||||||
fill-rule="evenodd"
|
{/if}
|
||||||
d="M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</label>
|
|
||||||
<button on:click={searchGo} type="submit" class="btn btn-neutral"
|
|
||||||
>{$t('navbar.search')}</button
|
|
||||||
>
|
|
||||||
</form>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import type { Adventure, OpenStreetMapPlace } from '$lib/types';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import { fail } from '@sveltejs/kit';
|
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { appVersion } from '$lib/config';
|
|
||||||
|
|
||||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||||
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
export const load = (async (event) => {
|
export const load = (async (event) => {
|
||||||
|
if (!event.locals.user) {
|
||||||
|
return redirect(302, '/login');
|
||||||
|
}
|
||||||
|
|
||||||
const query = event.url.searchParams.get('query');
|
const query = event.url.searchParams.get('query');
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue