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:
parent
433599dc20
commit
aa216f5688
7 changed files with 93 additions and 46 deletions
|
@ -1,7 +1,7 @@
|
||||||
import os
|
import os
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.html import mark_safe
|
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 worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity
|
||||||
from allauth.account.decorators import secure_admin_login
|
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(Category, CategoryAdmin)
|
||||||
admin.site.register(City, CityAdmin)
|
admin.site.register(City, CityAdmin)
|
||||||
admin.site.register(VisitedCity)
|
admin.site.register(VisitedCity)
|
||||||
|
admin.site.register(Attachment)
|
||||||
|
|
||||||
admin.site.site_header = 'AdventureLog Admin'
|
admin.site.site_header = 'AdventureLog Admin'
|
||||||
admin.site.site_title = 'AdventureLog Admin Site'
|
admin.site.site_title = 'AdventureLog Admin Site'
|
||||||
|
|
26
backend/server/adventures/migrations/0018_attachment.py
Normal file
26
backend/server/adventures/migrations/0018_attachment.py
Normal 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -288,6 +288,16 @@ class AdventureImage(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.image.url
|
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):
|
class Category(models.Model):
|
||||||
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
||||||
user_id = models.ForeignKey(
|
user_id = models.ForeignKey(
|
||||||
|
|
|
@ -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
|
|
41
backend/server/adventures/utils/file_permissions.py
Normal file
41
backend/server/adventures/utils/file_permissions.py
Normal 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
|
|
@ -81,8 +81,6 @@ MIDDLEWARE = (
|
||||||
# disable verifications for new users
|
# disable verifications for new users
|
||||||
ACCOUNT_EMAIL_VERIFICATION = 'none'
|
ACCOUNT_EMAIL_VERIFICATION = 'none'
|
||||||
|
|
||||||
ALLAUTH_UI_THEME = "night"
|
|
||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
'default': {
|
'default': {
|
||||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||||
|
|
|
@ -3,9 +3,8 @@ from django.middleware.csrf import get_token
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpResponse, HttpResponseForbidden
|
from django.http import HttpResponse, HttpResponseForbidden
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.views.static import serve
|
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):
|
def get_csrf_token(request):
|
||||||
csrf_token = get_token(request)
|
csrf_token = get_token(request)
|
||||||
|
@ -14,25 +13,30 @@ 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')})
|
||||||
|
|
||||||
|
protected_paths = ['images/', 'attachments/']
|
||||||
|
|
||||||
def serve_protected_media(request, path):
|
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]
|
image_id = path.split('/')[1]
|
||||||
user = request.user
|
user = request.user
|
||||||
if checkAdventureImagePermission(image_id, user):
|
media_type = path.split('/')[0] + '/'
|
||||||
|
if checkFilePermission(image_id, user, media_type):
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
# In debug mode, serve the file directly
|
# In debug mode, serve the file directly
|
||||||
return serve(request, path, document_root=settings.MEDIA_ROOT)
|
return serve(request, path, document_root=settings.MEDIA_ROOT)
|
||||||
else:
|
else:
|
||||||
# In production, use X-Accel-Redirect
|
# In production, use X-Accel-Redirect to serve the file using Nginx
|
||||||
response = HttpResponse()
|
response = HttpResponse()
|
||||||
response['Content-Type'] = ''
|
response['Content-Type'] = ''
|
||||||
response['X-Accel-Redirect'] = '/protectedMedia/' + path
|
response['X-Accel-Redirect'] = '/protectedMedia/' + path
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
return HttpResponseForbidden()
|
return HttpResponseForbidden()
|
||||||
|
else:
|
||||||
|
if settings.DEBUG:
|
||||||
|
return serve(request, path, document_root=settings.MEDIA_ROOT)
|
||||||
else:
|
else:
|
||||||
response = HttpResponse()
|
response = HttpResponse()
|
||||||
response['Content-Type'] = ''
|
response['Content-Type'] = ''
|
||||||
response['X-Accel-Redirect'] = '/protectedMedia/' + path
|
response['X-Accel-Redirect'] = '/protectedMedia/' + path
|
||||||
return response
|
return response
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue