1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-09 07:05:18 +02:00

feat: enhance permissions and validation in activity, trail, and visit views; add unique constraint to CollectionInvite model

This commit is contained in:
Sean Morley 2025-08-07 19:05:10 -04:00
parent 1395efa389
commit 6c683893e4
10 changed files with 83 additions and 21 deletions

View file

@ -0,0 +1,19 @@
# Generated by Django 5.2.2 on 2025-08-07 16:00
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('adventures', '0059_alter_activity_options'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterUniqueTogether(
name='collectioninvite',
unique_together={('collection', 'invited_user')},
),
]

View file

@ -267,8 +267,16 @@ class CollectionInvite(models.Model):
def __str__(self):
return f"Invite for {self.invited_user.username} to {self.collection.name}"
def clean(self):
if self.collection.user == self.invited_user:
raise ValidationError("You cannot invite yourself to your own collection.")
# dont allow if the user is already shared with the collection
if self.invited_user in self.collection.shared_with.all():
raise ValidationError("This user is already shared with the collection.")
class Meta:
verbose_name = "Collection Invite"
unique_together = ('collection', 'invited_user')
class Collection(models.Model):
#id = models.AutoField(primary_key=True)

View file

@ -39,12 +39,12 @@ class ActivityViewSet(viewsets.ModelViewSet):
Set the user when creating an activity.
"""
visit = serializer.validated_data.get('visit')
location = visit.location if visit else None
location = visit.location
if location and not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
raise PermissionDenied("You do not have permission to add an activity to this location.")
serializer.save(user=self.request.user)
serializer.save(user=location.user)
def perform_update(self, serializer):
instance = serializer.instance

View file

@ -21,7 +21,7 @@ class TrailViewSet(viewsets.ModelViewSet):
user = self.request.user
if not user or not user.is_authenticated:
return Trail.objects.none()
raise PermissionDenied("You must be authenticated to view trails.")
# Build the filter for accessible locations
location_filter = Q(location__user=user) # User owns the location
@ -37,13 +37,14 @@ class TrailViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
location = serializer.validated_data.get('location')
# Optional: import this if not in the same file
# from adventures.permissions import IsOwnerOrSharedWithFullAccess
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
raise PermissionDenied("You do not have permission to add a trail to this location.")
serializer.save(user=self.request.user)
# dont allow a user who does not own the location to attach a wanderer trail
if location.user != self.request.user and serializer.validated_data.get('wanderer_id'):
raise PermissionDenied("You cannot attach a wanderer trail to a location you do not own.")
serializer.save(user=location.user)
def perform_update(self, serializer):
instance = serializer.instance

View file

@ -21,7 +21,7 @@ class VisitViewSet(viewsets.ModelViewSet):
user = self.request.user
if not user or not user.is_authenticated:
return Visit.objects.none()
raise PermissionDenied("You must be authenticated to view visits.")
# Build the filter for accessible locations
location_filter = Q(location__user=user) # User owns the location

View file

@ -36,6 +36,9 @@ class WandererIntegrationViewSet(viewsets.ViewSet):
if WandererIntegration.objects.filter(user=request.user).exists():
raise ValidationError("Wanderer integration already exists. Use UPDATE instead.")
if not request.user.is_authenticated:
raise ValidationError("You must be authenticated to create a Wanderer integration.")
server_url = request.data.get("server_url")
username = request.data.get("username")
password = request.data.get("password")
@ -66,6 +69,12 @@ class WandererIntegrationViewSet(viewsets.ViewSet):
def update(self, request, pk=None):
inst = self._get_obj()
if not inst:
raise NotFound("Wanderer integration not found.")
if not self.request.user.is_authenticated:
raise ValidationError("You must be authenticated to update the integration.")
changed = False
for field in ("server_url", "username"):
if field in request.data and getattr(inst, field) != request.data[field]:
@ -94,6 +103,12 @@ class WandererIntegrationViewSet(viewsets.ViewSet):
@action(detail=False, methods=["post"])
def disable(self, request):
inst = self._get_obj()
if not inst:
raise NotFound("Wanderer integration not found.")
if not self.request.user.is_authenticated:
raise ValidationError("You must be authenticated to disable the integration.")
inst.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@ -101,6 +116,9 @@ class WandererIntegrationViewSet(viewsets.ViewSet):
def refresh(self, request):
inst = self._get_obj()
if not self.request.user.is_authenticated:
raise ValidationError("You must be authenticated to refresh the integration.")
password = request.data.get("password")
try:
session = get_valid_session(inst, password_for_reauth=password)
@ -117,6 +135,9 @@ class WandererIntegrationViewSet(viewsets.ViewSet):
def trails(self, request):
inst = self._get_obj()
if not self.request.user.is_authenticated:
raise ValidationError("You must be authenticated to access trails.")
# Check if we need to prompt for password
password = request.query_params.get("password") # Allow password via query param if needed

View file

@ -79,6 +79,9 @@ def get_valid_session(integration: WandererIntegration, password_for_reauth: str
now = django_timezone.now()
session = requests.Session()
if not integration:
raise IntegrationError("No Wanderer integration found.")
# Check if we have a valid token
if integration.token and integration.token_expiry and integration.token_expiry > now:
logger.debug("Using existing valid token")

View file

@ -252,6 +252,7 @@
<LocationDetails
currentUser={user}
initialLocation={location}
{collection}
bind:editingLocation={location}
on:back={() => {
steps[1].selected = false;
@ -281,6 +282,7 @@
bind:images={location.images}
bind:attachments={location.attachments}
bind:trails={location.trails}
userIsOwner={user?.uuid === location.user?.uuid}
on:back={() => {
steps[2].selected = false;
steps[1].selected = true;

View file

@ -61,6 +61,7 @@
longitude: number | null;
location: string;
tags: string[];
collections?: string[];
} = {
name: '',
category: null,
@ -71,7 +72,8 @@
latitude: null,
longitude: null,
location: '',
tags: []
tags: [],
collections: []
};
let user: User | null = null;
@ -84,6 +86,7 @@
export let initialLocation: any = null;
export let currentUser: any = null;
export let editingLocation: any = null;
export let collection: Collection | null = null;
$: user = currentUser;
$: locationToEdit = editingLocation;
@ -317,6 +320,9 @@
if (location.longitude !== null && typeof location.longitude === 'number') {
location.longitude = parseFloat(location.longitude.toFixed(6));
}
if (collection && collection.id) {
location.collections = [collection.id];
}
// either a post or a patch depending on whether we're editing or creating
if (locationToEdit && locationToEdit.id) {

View file

@ -1,5 +1,5 @@
<script lang="ts">
import type { Attachment, ContentImage, Trail, WandererTrail } from '$lib/types';
import type { Attachment, ContentImage, Trail, User, WandererTrail } from '$lib/types';
import { createEventDispatcher, onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { deserialize } from '$app/forms';
@ -37,7 +37,7 @@
export let trails: Trail[] = [];
export let itemId: string = '';
export let measurementSystem: 'metric' | 'imperial' = 'metric';
export let userIsOwner: boolean = false;
// Component state
let fileInput: HTMLInputElement;
let attachmentFileInput: HTMLInputElement;
@ -1014,6 +1014,7 @@
<PlusIcon class="w-4 h-4" />
Add Trail
</button>
{#if userIsOwner}
<button
class="btn btn-accent btn-sm gap-2"
on:click={() => {
@ -1023,6 +1024,7 @@
<PlusIcon class="w-4 h-4" />
Add Wanderer Trail
</button>
{/if}
</div>
</div>