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

@@ -21,10 +21,10 @@ const authStore = useAuthStore()
const orgId = computed(() => authStore.currentOrganisation?.id ?? '')
const eventId = computed(() => String((route.params as { id: string }).id))
const { data: crowdLists, isLoading, isError, refetch } = useCrowdLists(eventId)
const { data: crowdLists, isLoading, isError, refetch } = useCrowdLists(orgId, eventId)
const { data: crowdTypes } = useCrowdTypeList(orgId)
const { data: companies } = useCompanies(orgId)
const { mutate: deleteCrowdList, isPending: isDeleting } = useDeleteCrowdList(eventId)
const { mutate: deleteCrowdList, isPending: isDeleting } = useDeleteCrowdList(orgId, eventId)
// Lookup maps for resolving IDs to names
const crowdTypeMap = computed(() => {

View File

@@ -29,7 +29,7 @@ const filters = computed(() => ({
status: filterStatus.value || undefined,
}))
const { data: personsResponse, isLoading, isError, refetch } = usePersonList(eventId, filters)
const { data: personsResponse, isLoading, isError, refetch } = usePersonList(orgId, eventId, filters)
const { data: crowdTypes } = useCrowdTypeList(orgId)
const persons = computed(() => personsResponse.value?.data ?? [])
@@ -123,8 +123,8 @@ const isDeleteDialogOpen = ref(false)
const deletingPerson = ref<Person | null>(null)
// Mutations
const { mutate: approvePerson } = useApprovePerson(eventId)
const { mutate: deletePerson, isPending: isDeleting } = useDeletePerson(eventId)
const { mutate: approvePerson } = useApprovePerson(orgId, eventId)
const { mutate: deletePerson, isPending: isDeleting } = useDeletePerson(orgId, eventId)
const showSuccess = ref(false)
const successMessage = ref('')

View File

@@ -35,11 +35,11 @@ const eventId = computed(() => String((route.params as { id: string }).id))
const { mutate: updateEvent, isPending: isUpdatingEvent } = useUpdateEvent(orgId, eventId)
// Registration form fields
const { data: fields, isLoading } = useRegistrationFormFields(eventId)
const { mutate: createField, isPending: isCreating } = useCreateRegistrationFormField(eventId)
const { mutate: updateField, isPending: isUpdating } = useUpdateRegistrationFormField(eventId)
const { mutate: deleteField, isPending: isDeleting } = useDeleteRegistrationFormField(eventId)
const { mutate: reorderFields } = useReorderRegistrationFormFields(eventId)
const { data: fields, isLoading } = useRegistrationFormFields(orgId, eventId)
const { mutate: createField, isPending: isCreating } = useCreateRegistrationFormField(orgId, eventId)
const { mutate: updateField, isPending: isUpdating } = useUpdateRegistrationFormField(orgId, eventId)
const { mutate: deleteField, isPending: isDeleting } = useDeleteRegistrationFormField(orgId, eventId)
const { mutate: reorderFields } = useReorderRegistrationFormFields(orgId, eventId)
// Local draggable list synced from query
const localFields = ref<RegistrationFormField[]>([])

View File

@@ -20,8 +20,8 @@ const authStore = useAuthStore()
const orgId = computed(() => authStore.currentOrganisation?.id ?? '')
const eventId = computed(() => String((route.params as { id: string }).id))
const { data: timeSlots, isLoading, isError, refetch } = useTimeSlotList(eventId)
const { mutate: deleteTimeSlotMutation, isPending: isDeletingTimeSlot } = useDeleteTimeSlot(eventId)
const { data: timeSlots, isLoading, isError, refetch } = useTimeSlotList(orgId, eventId)
const { mutate: deleteTimeSlotMutation, isPending: isDeletingTimeSlot } = useDeleteTimeSlot(orgId, eventId)
// Load children for festivals — needed for time slot context chips
const { data: children } = useEventChildren(orgId, eventId)