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:
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):
|
def __str__(self):
|
||||||
return f"Invite for {self.invited_user.username} to {self.collection.name}"
|
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:
|
class Meta:
|
||||||
verbose_name = "Collection Invite"
|
verbose_name = "Collection Invite"
|
||||||
|
unique_together = ('collection', 'invited_user')
|
||||||
|
|
||||||
class Collection(models.Model):
|
class Collection(models.Model):
|
||||||
#id = models.AutoField(primary_key=True)
|
#id = models.AutoField(primary_key=True)
|
||||||
|
|
|
@ -39,12 +39,12 @@ class ActivityViewSet(viewsets.ModelViewSet):
|
||||||
Set the user when creating an activity.
|
Set the user when creating an activity.
|
||||||
"""
|
"""
|
||||||
visit = serializer.validated_data.get('visit')
|
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):
|
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.")
|
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):
|
def perform_update(self, serializer):
|
||||||
instance = serializer.instance
|
instance = serializer.instance
|
||||||
|
|
|
@ -21,7 +21,7 @@ class TrailViewSet(viewsets.ModelViewSet):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
|
|
||||||
if not user or not user.is_authenticated:
|
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
|
# Build the filter for accessible locations
|
||||||
location_filter = Q(location__user=user) # User owns the location
|
location_filter = Q(location__user=user) # User owns the location
|
||||||
|
@ -37,13 +37,14 @@ class TrailViewSet(viewsets.ModelViewSet):
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
location = serializer.validated_data.get('location')
|
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):
|
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
|
||||||
raise PermissionDenied("You do not have permission to add a trail to this 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):
|
def perform_update(self, serializer):
|
||||||
instance = serializer.instance
|
instance = serializer.instance
|
||||||
|
|
|
@ -21,7 +21,7 @@ class VisitViewSet(viewsets.ModelViewSet):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
|
|
||||||
if not user or not user.is_authenticated:
|
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
|
# Build the filter for accessible locations
|
||||||
location_filter = Q(location__user=user) # User owns the location
|
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():
|
if WandererIntegration.objects.filter(user=request.user).exists():
|
||||||
raise ValidationError("Wanderer integration already exists. Use UPDATE instead.")
|
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")
|
server_url = request.data.get("server_url")
|
||||||
username = request.data.get("username")
|
username = request.data.get("username")
|
||||||
password = request.data.get("password")
|
password = request.data.get("password")
|
||||||
|
@ -66,6 +69,12 @@ class WandererIntegrationViewSet(viewsets.ViewSet):
|
||||||
|
|
||||||
def update(self, request, pk=None):
|
def update(self, request, pk=None):
|
||||||
inst = self._get_obj()
|
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
|
changed = False
|
||||||
for field in ("server_url", "username"):
|
for field in ("server_url", "username"):
|
||||||
if field in request.data and getattr(inst, field) != request.data[field]:
|
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"])
|
@action(detail=False, methods=["post"])
|
||||||
def disable(self, request):
|
def disable(self, request):
|
||||||
inst = self._get_obj()
|
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()
|
inst.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@ -101,6 +116,9 @@ class WandererIntegrationViewSet(viewsets.ViewSet):
|
||||||
def refresh(self, request):
|
def refresh(self, request):
|
||||||
inst = self._get_obj()
|
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")
|
password = request.data.get("password")
|
||||||
try:
|
try:
|
||||||
session = get_valid_session(inst, password_for_reauth=password)
|
session = get_valid_session(inst, password_for_reauth=password)
|
||||||
|
@ -117,6 +135,9 @@ class WandererIntegrationViewSet(viewsets.ViewSet):
|
||||||
def trails(self, request):
|
def trails(self, request):
|
||||||
inst = self._get_obj()
|
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
|
# Check if we need to prompt for password
|
||||||
password = request.query_params.get("password") # Allow password via query param if needed
|
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()
|
now = django_timezone.now()
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
|
|
||||||
|
if not integration:
|
||||||
|
raise IntegrationError("No Wanderer integration found.")
|
||||||
|
|
||||||
# Check if we have a valid token
|
# Check if we have a valid token
|
||||||
if integration.token and integration.token_expiry and integration.token_expiry > now:
|
if integration.token and integration.token_expiry and integration.token_expiry > now:
|
||||||
logger.debug("Using existing valid token")
|
logger.debug("Using existing valid token")
|
||||||
|
|
|
@ -252,6 +252,7 @@
|
||||||
<LocationDetails
|
<LocationDetails
|
||||||
currentUser={user}
|
currentUser={user}
|
||||||
initialLocation={location}
|
initialLocation={location}
|
||||||
|
{collection}
|
||||||
bind:editingLocation={location}
|
bind:editingLocation={location}
|
||||||
on:back={() => {
|
on:back={() => {
|
||||||
steps[1].selected = false;
|
steps[1].selected = false;
|
||||||
|
@ -281,6 +282,7 @@
|
||||||
bind:images={location.images}
|
bind:images={location.images}
|
||||||
bind:attachments={location.attachments}
|
bind:attachments={location.attachments}
|
||||||
bind:trails={location.trails}
|
bind:trails={location.trails}
|
||||||
|
userIsOwner={user?.uuid === location.user?.uuid}
|
||||||
on:back={() => {
|
on:back={() => {
|
||||||
steps[2].selected = false;
|
steps[2].selected = false;
|
||||||
steps[1].selected = true;
|
steps[1].selected = true;
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
longitude: number | null;
|
longitude: number | null;
|
||||||
location: string;
|
location: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
collections?: string[];
|
||||||
} = {
|
} = {
|
||||||
name: '',
|
name: '',
|
||||||
category: null,
|
category: null,
|
||||||
|
@ -71,7 +72,8 @@
|
||||||
latitude: null,
|
latitude: null,
|
||||||
longitude: null,
|
longitude: null,
|
||||||
location: '',
|
location: '',
|
||||||
tags: []
|
tags: [],
|
||||||
|
collections: []
|
||||||
};
|
};
|
||||||
|
|
||||||
let user: User | null = null;
|
let user: User | null = null;
|
||||||
|
@ -84,6 +86,7 @@
|
||||||
export let initialLocation: any = null;
|
export let initialLocation: any = null;
|
||||||
export let currentUser: any = null;
|
export let currentUser: any = null;
|
||||||
export let editingLocation: any = null;
|
export let editingLocation: any = null;
|
||||||
|
export let collection: Collection | null = null;
|
||||||
|
|
||||||
$: user = currentUser;
|
$: user = currentUser;
|
||||||
$: locationToEdit = editingLocation;
|
$: locationToEdit = editingLocation;
|
||||||
|
@ -317,6 +320,9 @@
|
||||||
if (location.longitude !== null && typeof location.longitude === 'number') {
|
if (location.longitude !== null && typeof location.longitude === 'number') {
|
||||||
location.longitude = parseFloat(location.longitude.toFixed(6));
|
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
|
// either a post or a patch depending on whether we're editing or creating
|
||||||
if (locationToEdit && locationToEdit.id) {
|
if (locationToEdit && locationToEdit.id) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<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 { createEventDispatcher, onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { deserialize } from '$app/forms';
|
import { deserialize } from '$app/forms';
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
export let trails: Trail[] = [];
|
export let trails: Trail[] = [];
|
||||||
export let itemId: string = '';
|
export let itemId: string = '';
|
||||||
export let measurementSystem: 'metric' | 'imperial' = 'metric';
|
export let measurementSystem: 'metric' | 'imperial' = 'metric';
|
||||||
|
export let userIsOwner: boolean = false;
|
||||||
// Component state
|
// Component state
|
||||||
let fileInput: HTMLInputElement;
|
let fileInput: HTMLInputElement;
|
||||||
let attachmentFileInput: HTMLInputElement;
|
let attachmentFileInput: HTMLInputElement;
|
||||||
|
@ -1014,15 +1014,17 @@
|
||||||
<PlusIcon class="w-4 h-4" />
|
<PlusIcon class="w-4 h-4" />
|
||||||
Add Trail
|
Add Trail
|
||||||
</button>
|
</button>
|
||||||
<button
|
{#if userIsOwner}
|
||||||
class="btn btn-accent btn-sm gap-2"
|
<button
|
||||||
on:click={() => {
|
class="btn btn-accent btn-sm gap-2"
|
||||||
doShowWandererForm();
|
on:click={() => {
|
||||||
}}
|
doShowWandererForm();
|
||||||
>
|
}}
|
||||||
<PlusIcon class="w-4 h-4" />
|
>
|
||||||
Add Wanderer Trail
|
<PlusIcon class="w-4 h-4" />
|
||||||
</button>
|
Add Wanderer Trail
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue