mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-08 22:55:19 +02:00
feat: enhance permissions and validation in activity, trail, and visit views; add unique constraint to CollectionInvite model
This commit is contained in:
parent
1395efa389
commit
6c683893e4
10 changed files with 83 additions and 21 deletions
|
@ -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')},
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,15 +1014,17 @@
|
|||
<PlusIcon class="w-4 h-4" />
|
||||
Add Trail
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-accent btn-sm gap-2"
|
||||
on:click={() => {
|
||||
doShowWandererForm();
|
||||
}}
|
||||
>
|
||||
<PlusIcon class="w-4 h-4" />
|
||||
Add Wanderer Trail
|
||||
</button>
|
||||
{#if userIsOwner}
|
||||
<button
|
||||
class="btn btn-accent btn-sm gap-2"
|
||||
on:click={() => {
|
||||
doShowWandererForm();
|
||||
}}
|
||||
>
|
||||
<PlusIcon class="w-4 h-4" />
|
||||
Add Wanderer Trail
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue