security: A01-13 — nest all event routes under organisation prefix

Move all authenticated organiser-facing event sub-resource routes from
/events/{event}/... to /organisations/{organisation}/events/{event}/...
to enforce multi-tenancy at the routing layer.

Changes:
- Routes: restructured api.php to nest all event sub-resources under
  the existing organisation prefix group
- Controllers: added Organisation parameter and VerifiesOrganisationEvent
  trait to all 12 affected controllers (sections, time-slots, shifts,
  persons, crowd-lists, locations, shift-assignments, registration-fields,
  availabilities, field-values, section-preferences, stats)
- Tests: updated all 20 feature test files with new route paths
- Frontend: updated 8 API composables and 20 Vue components/pages
- API.md: updated documentation to reflect new route structure

Portal routes, public routes (volunteer-register), and invitation routes
remain unchanged as they operate without organisation context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 08:16:36 +02:00
parent 51e5dd6fcb
commit 7932e53daf
64 changed files with 726 additions and 568 deletions

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { useAssignablePersons, useAssignPersonToShift } from '@/composables/api/useShiftAssignments'
import { getApiErrorMessage } from '@/lib/apiErrors'
import { useAuthStore } from '@/stores/useAuthStore'
import type { AssignablePerson } from '@/types/shiftAssignment'
import type { Shift } from '@/types/section'
@@ -16,11 +17,13 @@ const emit = defineEmits<{
const modelValue = defineModel<boolean>({ required: true })
const authStore = useAuthStore()
const orgIdRef = computed(() => authStore.currentOrganisation?.id ?? '')
const eventIdRef = computed(() => props.eventId)
const shiftIdRef = computed(() => props.shift?.id ?? '')
const { data: assignableData, isLoading } = useAssignablePersons(eventIdRef, shiftIdRef)
const { mutateAsync: assignPerson, isPending: isAssigning } = useAssignPersonToShift(eventIdRef)
const { data: assignableData, isLoading } = useAssignablePersons(orgIdRef, eventIdRef, shiftIdRef)
const { mutateAsync: assignPerson, isPending: isAssigning } = useAssignPersonToShift(orgIdRef, eventIdRef)
// Search and filters
const searchQuery = ref('')

View File

@@ -9,6 +9,7 @@ import {
} from '@/composables/api/useShiftAssignments'
import AssignPersonDialog from '@/components/shifts/AssignPersonDialog.vue'
import { getApiErrorMessage } from '@/lib/apiErrors'
import { useAuthStore } from '@/stores/useAuthStore'
import { useShiftDetailStore } from '@/stores/useShiftDetailStore'
import { ShiftAssignmentStatus } from '@/types/shiftAssignment'
import type { ShiftAssignment } from '@/types/shiftAssignment'
@@ -21,7 +22,9 @@ const props = defineProps<{
const modelValue = defineModel<boolean>({ required: true })
const authStore = useAuthStore()
const store = useShiftDetailStore()
const orgIdRef = computed(() => authStore.currentOrganisation?.id ?? '')
const eventIdRef = computed(() => props.eventId)
// Fetch assignments filtered by this shift
@@ -34,16 +37,16 @@ const {
isLoading: assignmentsLoading,
isError: assignmentsError,
refetch: refetchAssignments,
} = useShiftAssignmentList(eventIdRef, filters)
} = useShiftAssignmentList(orgIdRef, eventIdRef, filters)
const assignments = computed(() => assignmentsResponse.value?.data ?? [])
// Mutations
const { mutate: approveAssignment, isPending: isApproving } = useApproveAssignment(eventIdRef)
const { mutate: rejectAssignment, isPending: isRejecting } = useRejectAssignment(eventIdRef)
const { mutate: cancelAssignment, isPending: isCancelling } = useCancelAssignment(eventIdRef)
const { mutate: bulkApprove, isPending: isBulkApproving } = useBulkApproveAssignments(eventIdRef)
const { mutateAsync: assignPersonMutation } = useAssignPersonToShift(eventIdRef)
const { mutate: approveAssignment, isPending: isApproving } = useApproveAssignment(orgIdRef, eventIdRef)
const { mutate: rejectAssignment, isPending: isRejecting } = useRejectAssignment(orgIdRef, eventIdRef)
const { mutate: cancelAssignment, isPending: isCancelling } = useCancelAssignment(orgIdRef, eventIdRef)
const { mutate: bulkApprove, isPending: isBulkApproving } = useBulkApproveAssignments(orgIdRef, eventIdRef)
const { mutateAsync: assignPersonMutation } = useAssignPersonToShift(orgIdRef, eventIdRef)
// Re-assign
const reassigning = ref<string | null>(null)