1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-23 14:59:36 +02:00

feat: add Attachment model and implement file permission checks for media access

This commit is contained in:
Sean Morley 2025-01-18 20:06:12 -05:00
parent 433599dc20
commit aa216f5688
7 changed files with 93 additions and 46 deletions

View file

@ -1,7 +1,7 @@
import os
from django.contrib import admin
from django.utils.html import mark_safe
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category, Attachment
from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity
from allauth.account.decorators import secure_admin_login
@ -139,6 +139,7 @@ admin.site.register(AdventureImage, AdventureImageAdmin)
admin.site.register(Category, CategoryAdmin)
admin.site.register(City, CityAdmin)
admin.site.register(VisitedCity)
admin.site.register(Attachment)
admin.site.site_header = 'AdventureLog Admin'
admin.site.site_title = 'AdventureLog Admin Site'

View file

@ -0,0 +1,26 @@
# Generated by Django 5.0.8 on 2025-01-19 00:39
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0017_adventureimage_is_primary'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Attachment',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('file', models.FileField(upload_to='attachments/')),
('adventure', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='adventures.adventure')),
('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -287,6 +287,16 @@ class AdventureImage(models.Model):
def __str__(self):
return self.image.url
class Attachment(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
file = models.FileField(upload_to='attachments/')
adventure = models.ForeignKey(Adventure, related_name='attachments', on_delete=models.CASCADE)
def __str__(self):
return self.file.url
class Category(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)

View file

@ -1,33 +0,0 @@
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

View file

@ -0,0 +1,41 @@
from adventures.models import AdventureImage, Attachment
protected_paths = ['images/', 'attachments/']
def checkFilePermission(fileId, user, mediaType):
if mediaType not in protected_paths:
return True
if mediaType == 'images/':
try:
# Construct the full relative path to match the database field
image_path = f"images/{fileId}"
# 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:
return False
elif mediaType == 'attachments/':
try:
# Construct the full relative path to match the database field
attachment_path = f"attachments/{fileId}"
# Fetch the Attachment object
attachment = Attachment.objects.get(file=attachment_path).adventure
if attachment.is_public:
return True
elif attachment.user_id == user:
return True
elif attachment.collection:
if attachment.collection.shared_with.filter(id=user.id).exists():
return True
else:
return False
except Attachment.DoesNotExist:
return False

View file

@ -81,8 +81,6 @@ MIDDLEWARE = (
# disable verifications for new users
ACCOUNT_EMAIL_VERIFICATION = 'none'
ALLAUTH_UI_THEME = "night"
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',

View file

@ -3,9 +3,8 @@ from django.middleware.csrf import get_token
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
from adventures.utils.file_permissions import checkFilePermission
def get_csrf_token(request):
csrf_token = get_token(request)
@ -14,16 +13,19 @@ def get_csrf_token(request):
def get_public_url(request):
return JsonResponse({'PUBLIC_URL': getenv('PUBLIC_URL')})
protected_paths = ['images/', 'attachments/']
def serve_protected_media(request, path):
if path.startswith('images/'):
if any([path.startswith(protected_path) for protected_path in protected_paths]):
image_id = path.split('/')[1]
user = request.user
if checkAdventureImagePermission(image_id, user):
media_type = path.split('/')[0] + '/'
if checkFilePermission(image_id, user, media_type):
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
# In production, use X-Accel-Redirect to serve the file using Nginx
response = HttpResponse()
response['Content-Type'] = ''
response['X-Accel-Redirect'] = '/protectedMedia/' + path
@ -31,8 +33,10 @@ def serve_protected_media(request, path):
else:
return HttpResponseForbidden()
else:
response = HttpResponse()
response['Content-Type'] = ''
response['X-Accel-Redirect'] = '/protectedMedia/' + path
return response
if settings.DEBUG:
return serve(request, path, document_root=settings.MEDIA_ROOT)
else:
response = HttpResponse()
response['Content-Type'] = ''
response['X-Accel-Redirect'] = '/protectedMedia/' + path
return response