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

feat: enhance Immich integration to support image downloading for shared users and improve access control for adventure images

This commit is contained in:
Sean Morley 2025-06-03 17:59:29 -04:00
parent b336a24401
commit 442a7724a0
2 changed files with 125 additions and 14 deletions

View file

@ -3,9 +3,14 @@ from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from django.db.models import Q from django.db.models import Q
from django.core.files.base import ContentFile
from adventures.models import Adventure, AdventureImage from adventures.models import Adventure, AdventureImage
from adventures.serializers import AdventureImageSerializer from adventures.serializers import AdventureImageSerializer
from integrations.models import ImmichIntegration
import uuid import uuid
import requests
import tempfile
import os
class AdventureImageViewSet(viewsets.ModelViewSet): class AdventureImageViewSet(viewsets.ModelViewSet):
serializer_class = AdventureImageSerializer serializer_class = AdventureImageSerializer
@ -56,6 +61,77 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
else: else:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN) return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
# Handle Immich ID for shared users by downloading the image
if (request.user != adventure.user_id and
'immich_id' in request.data and
request.data.get('immich_id')):
immich_id = request.data.get('immich_id')
# Get the shared user's Immich integration
try:
user_integration = ImmichIntegration.objects.get(user_id=request.user)
except ImmichIntegration.DoesNotExist:
return Response({
"error": "No Immich integration found for your account. Please set up Immich integration first.",
"code": "immich_integration_not_found"
}, status=status.HTTP_400_BAD_REQUEST)
# Download the image from the shared user's Immich server
try:
immich_response = requests.get(
f'{user_integration.server_url}/assets/{immich_id}/thumbnail?size=preview',
headers={'x-api-key': user_integration.api_key},
timeout=10
)
immich_response.raise_for_status()
# Create a temporary file with the downloaded content
content_type = immich_response.headers.get('Content-Type', 'image/jpeg')
if not content_type.startswith('image/'):
return Response({
"error": "Invalid content type returned from Immich server.",
"code": "invalid_content_type"
}, status=status.HTTP_400_BAD_REQUEST)
# Determine file extension from content type
ext_map = {
'image/jpeg': '.jpg',
'image/png': '.png',
'image/webp': '.webp',
'image/gif': '.gif'
}
file_ext = ext_map.get(content_type, '.jpg')
filename = f"immich_{immich_id}{file_ext}"
# Create a Django ContentFile from the downloaded image
image_file = ContentFile(immich_response.content, name=filename)
# Modify request data to use the downloaded image instead of immich_id
request_data = request.data.copy()
request_data.pop('immich_id', None) # Remove immich_id
# Create the serializer with the modified data
serializer = self.get_serializer(data=request_data)
serializer.is_valid(raise_exception=True)
# Save with the downloaded image
adventure = serializer.validated_data['adventure']
serializer.save(user_id=adventure.user_id, image=image_file)
return Response(serializer.data, status=status.HTTP_201_CREATED)
except requests.exceptions.RequestException as e:
return Response({
"error": f"Failed to fetch image from Immich server: {str(e)}",
"code": "immich_fetch_failed"
}, status=status.HTTP_502_BAD_GATEWAY)
except Exception as e:
return Response({
"error": f"Unexpected error processing Immich image: {str(e)}",
"code": "immich_processing_error"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return super().create(request, *args, **kwargs) return super().create(request, *args, **kwargs)
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
@ -110,15 +186,25 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
except ValueError: except ValueError:
return Response({"error": "Invalid adventure ID"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "Invalid adventure ID"}, status=status.HTTP_400_BAD_REQUEST)
# Updated queryset to include images from adventures the user owns OR has shared access to
queryset = AdventureImage.objects.filter( queryset = AdventureImage.objects.filter(
Q(adventure__id=adventure_uuid) & Q(user_id=request.user) Q(adventure__id=adventure_uuid) & (
Q(adventure__user_id=request.user) | # User owns the adventure
Q(adventure__collection__shared_with=request.user) # User has shared access via collection
) )
).distinct()
serializer = self.get_serializer(queryset, many=True, context={'request': request}) serializer = self.get_serializer(queryset, many=True, context={'request': request})
return Response(serializer.data) return Response(serializer.data)
def get_queryset(self): def get_queryset(self):
return AdventureImage.objects.filter(user_id=self.request.user) # Updated to include images from adventures the user owns OR has shared access to
return AdventureImage.objects.filter(
Q(adventure__user_id=self.request.user) | # User owns the adventure
Q(adventure__collection__shared_with=self.request.user) # User has shared access via collection
).distinct()
def perform_create(self, serializer): def perform_create(self, serializer):
serializer.save(user_id=self.request.user) # Always set the image owner to the adventure owner, not the current user
adventure = serializer.validated_data['adventure']
serializer.save(user_id=adventure.user_id)

View file

@ -232,9 +232,12 @@ class ImmichIntegrationView(viewsets.ViewSet):
def get_by_integration(self, request, integration_id=None, imageid=None): def get_by_integration(self, request, integration_id=None, imageid=None):
""" """
GET an Immich image using the integration and asset ID. GET an Immich image using the integration and asset ID.
- Public adventures: accessible by anyone Access levels (in order of priority):
- Private adventures: accessible only to the owner 1. Public adventures: accessible by anyone
- No AdventureImage: owner can still view via integration 2. Private adventures in public collections: accessible by anyone
3. Private adventures in private collections shared with user: accessible by shared users
4. Private adventures: accessible only to the owner
5. No AdventureImage: owner can still view via integration
""" """
if not imageid or not integration_id: if not imageid or not integration_id:
return Response({ return Response({
@ -247,22 +250,45 @@ class ImmichIntegrationView(viewsets.ViewSet):
integration = get_object_or_404(ImmichIntegration, id=integration_id) integration = get_object_or_404(ImmichIntegration, id=integration_id)
owner_id = integration.user_id owner_id = integration.user_id
# Try to find the image entry # Try to find the image entry with collection and sharing information
image_entry = ( image_entry = (
AdventureImage.objects AdventureImage.objects
.filter(immich_id=imageid, user_id=owner_id) .filter(immich_id=imageid, user_id=owner_id)
.select_related('adventure') .select_related('adventure', 'adventure__collection')
.order_by('-adventure__is_public') # True (1) first, False (0) last .prefetch_related('adventure__collection__shared_with')
.order_by(
'-adventure__is_public', # Public adventures first
'-adventure__collection__is_public' # Then public collections
)
.first() .first()
) )
# Access control # Access control
if image_entry: if image_entry:
if image_entry.adventure.is_public: adventure = image_entry.adventure
collection = adventure.collection
# Determine access level
is_authorized = False
# Level 1: Public adventure (highest priority)
if adventure.is_public:
is_authorized = True is_authorized = True
# Level 2: Private adventure in public collection
elif collection and collection.is_public:
is_authorized = True
# Level 3: Owner access
elif request.user.is_authenticated and request.user.id == owner_id: elif request.user.is_authenticated and request.user.id == owner_id:
is_authorized = True is_authorized = True
else:
# Level 4: Shared collection access
elif (request.user.is_authenticated and collection and
collection.shared_with.filter(id=request.user.id).exists()):
is_authorized = True
if not is_authorized:
return Response({ return Response({
'message': 'This image belongs to a private adventure and you are not authorized.', 'message': 'This image belongs to a private adventure and you are not authorized.',
'error': True, 'error': True,
@ -276,7 +302,6 @@ class ImmichIntegrationView(viewsets.ViewSet):
'error': True, 'error': True,
'code': 'immich.not_found' 'code': 'immich.not_found'
}, status=status.HTTP_404_NOT_FOUND) }, status=status.HTTP_404_NOT_FOUND)
is_authorized = True # Integration owner fallback
# Fetch from Immich # Fetch from Immich
try: try: