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

@@ -49,7 +49,7 @@ Returns 422 with `errors`, `current_status`, `requested_status`, and `allowed_tr
## Event Stats
- `GET /events/{event}/stats` — aggregate dashboard counts for the event
- `GET /organisations/{org}/events/{event}/stats` — aggregate dashboard counts for the event
### Response
@@ -90,11 +90,11 @@ Returns 422 with `errors`, `current_status`, `requested_status`, and `allowed_tr
## Festival Sections
- `GET /events/{event}/sections`
- `POST /events/{event}/sections`
- `PUT /events/{event}/sections/{section}`
- `DELETE /events/{event}/sections/{section}`
- `POST /events/{event}/sections/reorder`
- `GET /organisations/{org}/events/{event}/sections`
- `POST /organisations/{org}/events/{event}/sections`
- `PUT /organisations/{org}/events/{event}/sections/{section}`
- `DELETE /organisations/{org}/events/{event}/sections/{section}`
- `POST /organisations/{org}/events/{event}/sections/reorder`
> **Festival context:** `{event}` can be a festival parent or a sub-event.
> On a festival parent, sections are for operational planning (build-up, teardown).
@@ -104,8 +104,8 @@ Returns 422 with `errors`, `current_status`, `requested_status`, and `allowed_tr
### Registration Settings (Festival-level bulk management)
- `GET /events/{event}/sections/registration-settings` — returns unique section names across the festival with registration visibility, description, and counts
- `PUT /events/{event}/sections/registration-settings` — bulk update registration visibility for a section name across all instances in the festival
- `GET /organisations/{org}/events/{event}/sections/registration-settings` — returns unique section names across the festival with registration visibility, description, and counts
- `PUT /organisations/{org}/events/{event}/sections/registration-settings` — bulk update registration visibility for a section name across all instances in the festival
#### GET Response
@@ -141,16 +141,16 @@ Auth: org_admin or event_manager on the event's organisation.
## Time Slots
- `GET /events/{event}/time-slots`
- `POST /events/{event}/time-slots`
- `PUT /events/{event}/time-slots/{timeSlot}`
- `DELETE /events/{event}/time-slots/{timeSlot}`
- `GET /organisations/{org}/events/{event}/time-slots`
- `POST /organisations/{org}/events/{event}/time-slots`
- `PUT /organisations/{org}/events/{event}/time-slots/{timeSlot}`
- `DELETE /organisations/{org}/events/{event}/time-slots/{timeSlot}`
> **Festival context:** `{event}` can be a festival parent or a sub-event.
> Festival-level time slots (operational: build-up, teardown, transitions) are separate
> from sub-event time slots (program-specific).
>
> `GET /events/{event}/time-slots` returns only the specified event's own time slots by
> `GET /organisations/{org}/events/{event}/time-slots` returns only the specified event's own time slots by
> default. For sub-events, pass `?include_parent=true` to also include the parent festival's
> time slots — each time slot is marked with a `source` field (`sub_event` or `festival`)
> and includes `event_name` for display grouping. This parameter has no effect on festivals
@@ -162,12 +162,12 @@ Auth: org_admin or event_manager on the event's organisation.
## Shifts
- `GET /events/{event}/sections/{section}/shifts`
- `POST /events/{event}/sections/{section}/shifts`
- `PUT /events/{event}/sections/{section}/shifts/{shift}`
- `DELETE /events/{event}/sections/{section}/shifts/{shift}`
- `POST /events/{event}/sections/{section}/shifts/{shift}/assign`
- `POST /events/{event}/sections/{section}/shifts/{shift}/claim`
- `GET /organisations/{org}/events/{event}/sections/{section}/shifts`
- `POST /organisations/{org}/events/{event}/sections/{section}/shifts`
- `PUT /organisations/{org}/events/{event}/sections/{section}/shifts/{shift}`
- `DELETE /organisations/{org}/events/{event}/sections/{section}/shifts/{shift}`
- `POST /organisations/{org}/events/{event}/sections/{section}/shifts/{shift}/assign`
- `POST /organisations/{org}/events/{event}/sections/{section}/shifts/{shift}/claim`
> **Festival context:** When managing shifts on a `cross_event` section, the `{event}`
> in the URL must be the parent festival's ID (matching `section.event_id`), not the
@@ -175,16 +175,16 @@ Auth: org_admin or event_manager on the event's organisation.
## Shift Assignments
- `GET /events/{event}/shift-assignments` — list assignments for event (paginated, 50/page)
- `POST /events/{event}/shift-assignments/{shiftAssignment}/approve` — approve pending assignment
- `POST /events/{event}/shift-assignments/{shiftAssignment}/reject` — reject pending assignment
- `POST /events/{event}/shift-assignments/{shiftAssignment}/cancel` — cancel assignment
- `POST /events/{event}/shift-assignments/bulk-approve` — bulk approve multiple assignments
- `GET /events/{event}/shifts/{shift}/assignable-persons` — list approved persons with availability status
- `GET /organisations/{org}/events/{event}/shift-assignments` — list assignments for event (paginated, 50/page)
- `POST /organisations/{org}/events/{event}/shift-assignments/{shiftAssignment}/approve` — approve pending assignment
- `POST /organisations/{org}/events/{event}/shift-assignments/{shiftAssignment}/reject` — reject pending assignment
- `POST /organisations/{org}/events/{event}/shift-assignments/{shiftAssignment}/cancel` — cancel assignment
- `POST /organisations/{org}/events/{event}/shift-assignments/bulk-approve` — bulk approve multiple assignments
- `GET /organisations/{org}/events/{event}/shifts/{shift}/assignable-persons` — list approved persons with availability status
### Assignable Persons
`GET /events/{event}/shifts/{shift}/assignable-persons`
`GET /organisations/{org}/events/{event}/shifts/{shift}/assignable-persons`
Returns all approved persons for the event with availability status for this shift's time slot.
Persons are sorted: available first, then unavailable (conflict), then already assigned.
@@ -234,7 +234,7 @@ Persons are sorted: available first, then unavailable (conflict), then already a
### Assign Body
`POST /events/{event}/sections/{section}/shifts/{shift}/assign`
`POST /organisations/{org}/events/{event}/sections/{section}/shifts/{shift}/assign`
```json
{ "person_id": "01JXYZ..." }
@@ -245,7 +245,7 @@ Validates: shift must be `open`, capacity not full (`slots_total`), no time slot
### Claim Body
`POST /events/{event}/sections/{section}/shifts/{shift}/claim`
`POST /organisations/{org}/events/{event}/sections/{section}/shifts/{shift}/claim`
```json
{ "person_id": "01JXYZ..." }
@@ -317,8 +317,8 @@ Response includes per-assignment result: `approved` or `skipped` (with reason).
## Volunteer Availabilities
- `GET /events/{event}/persons/{person}/availabilities` — list availabilities
- `POST /events/{event}/persons/{person}/availabilities/sync` — sync (replace all)
- `GET /organisations/{org}/events/{event}/persons/{person}/availabilities` — list availabilities
- `POST /organisations/{org}/events/{event}/persons/{person}/availabilities/sync` — sync (replace all)
### Sync Body
@@ -339,12 +339,12 @@ Validates:
## Persons
- `GET /events/{event}/persons`
- `POST /events/{event}/persons`
- `GET /events/{event}/persons/{person}`
- `PUT /events/{event}/persons/{person}`
- `POST /events/{event}/persons/{person}/approve`
- `DELETE /events/{event}/persons/{person}`
- `GET /organisations/{org}/events/{event}/persons`
- `POST /organisations/{org}/events/{event}/persons`
- `GET /organisations/{org}/events/{event}/persons/{person}`
- `PUT /organisations/{org}/events/{event}/persons/{person}`
- `POST /organisations/{org}/events/{event}/persons/{person}/approve`
- `DELETE /organisations/{org}/events/{event}/persons/{person}`
## Identity Matches
@@ -357,7 +357,7 @@ Validates:
### Detection
Matches are created automatically:
- When a person is created (via `POST /events/{event}/persons`) with an email matching an existing user → pending match created
- When a person is created (via `POST /organisations/{org}/events/{event}/persons`) with an email matching an existing user → pending match created
- When a new user account is created (invitation acceptance) with an email matching unlinked persons → pending matches created
No silent auto-linking. Every identity link requires explicit confirmation.
@@ -372,7 +372,7 @@ Response: `{ "confirmed": 2, "errors": [{ "match_id": "ulid3", "error": "User al
### PersonResource enrichment
`GET /events/{event}/persons` includes `pending_identity_match` inline when a pending match exists:
`GET /organisations/{org}/events/{event}/persons` includes `pending_identity_match` inline when a pending match exists:
```json
{
@@ -387,13 +387,13 @@ Response: `{ "confirmed": 2, "errors": [{ "match_id": "ulid3", "error": "User al
## Crowd Lists
- `GET /events/{event}/crowd-lists` — list all crowd lists for event (includes `persons_count`)
- `POST /events/{event}/crowd-lists` — create crowd list
- `PUT /events/{event}/crowd-lists/{list}` — update crowd list
- `DELETE /events/{event}/crowd-lists/{list}` — delete crowd list
- `GET /events/{event}/crowd-lists/{list}/persons` — list persons on a crowd list (paginated, 50/page, includes `crowd_list_pivot`)
- `POST /events/{event}/crowd-lists/{list}/persons` — add person to list
- `DELETE /events/{event}/crowd-lists/{list}/persons/{person}` — remove person from list
- `GET /organisations/{org}/events/{event}/crowd-lists` — list all crowd lists for event (includes `persons_count`)
- `POST /organisations/{org}/events/{event}/crowd-lists` — create crowd list
- `PUT /organisations/{org}/events/{event}/crowd-lists/{list}` — update crowd list
- `DELETE /organisations/{org}/events/{event}/crowd-lists/{list}` — delete crowd list
- `GET /organisations/{org}/events/{event}/crowd-lists/{list}/persons` — list persons on a crowd list (paginated, 50/page, includes `crowd_list_pivot`)
- `POST /organisations/{org}/events/{event}/crowd-lists/{list}/persons` — add person to list
- `DELETE /organisations/{org}/events/{event}/crowd-lists/{list}/persons/{person}` — remove person from list
### Create/Update Body
@@ -441,10 +441,10 @@ Response: `{ "confirmed": 2, "errors": [{ "match_id": "ulid3", "error": "User al
## Locations
- `GET /events/{event}/locations`
- `POST /events/{event}/locations`
- `PUT /events/{event}/locations/{location}`
- `DELETE /events/{event}/locations/{location}`
- `GET /organisations/{org}/events/{event}/locations`
- `POST /organisations/{org}/events/{event}/locations`
- `PUT /organisations/{org}/events/{event}/locations/{location}`
- `DELETE /organisations/{org}/events/{event}/locations/{location}`
## Person Tags (Organisation Settings)
@@ -467,8 +467,8 @@ Response: `{ "confirmed": 2, "errors": [{ "match_id": "ulid3", "error": "User al
### Person list tag filtering
- `GET /events/{event}/persons?tag={person_tag_id}` — filter persons by single tag
- `GET /events/{event}/persons?tags=ulid1,ulid2` — filter persons by multiple tags (AND logic: must have all)
- `GET /organisations/{org}/events/{event}/persons?tag={person_tag_id}` — filter persons by single tag
- `GET /organisations/{org}/events/{event}/persons?tags=ulid1,ulid2` — filter persons by multiple tags (AND logic: must have all)
## Public Registration Data
@@ -511,13 +511,13 @@ Response: `{ "confirmed": 2, "errors": [{ "match_id": "ulid3", "error": "User al
## Registration Form Fields (Event Settings)
- `GET /events/{event}/registration-fields` — list all fields (ordered by sort_order)
- `POST /events/{event}/registration-fields` — create field (manually or from template)
- `POST /events/{event}/registration-fields/from-template` — create field from template
- `PUT /events/{event}/registration-fields/{field}` — update field
- `DELETE /events/{event}/registration-fields/{field}` — delete field definition (answers preserved)
- `POST /events/{event}/registration-fields/reorder` — bulk reorder
- `POST /events/{event}/registration-fields/import-from-event` — copy fields from another event
- `GET /organisations/{org}/events/{event}/registration-fields` — list all fields (ordered by sort_order)
- `POST /organisations/{org}/events/{event}/registration-fields` — create field (manually or from template)
- `POST /organisations/{org}/events/{event}/registration-fields/from-template` — create field from template
- `PUT /organisations/{org}/events/{event}/registration-fields/{field}` — update field
- `DELETE /organisations/{org}/events/{event}/registration-fields/{field}` — delete field definition (answers preserved)
- `POST /organisations/{org}/events/{event}/registration-fields/reorder` — bulk reorder
- `POST /organisations/{org}/events/{event}/registration-fields/import-from-event` — copy fields from another event
### From-Template Body
@@ -541,8 +541,8 @@ For `tag_picker` fields: the API response includes `available_tags` array (from
## Person Field Values
- `GET /events/{event}/persons/{person}/field-values` — all answers for a person
- `PUT /events/{event}/persons/{person}/field-values` — bulk upsert answers
- `GET /organisations/{org}/events/{event}/persons/{person}/field-values` — all answers for a person
- `PUT /organisations/{org}/events/{event}/persons/{person}/field-values` — bulk upsert answers
### Bulk Upsert Body
@@ -561,8 +561,8 @@ Replaces all field values for this person in one request. Used by both the regis
## Person Section Preferences
- `GET /events/{event}/persons/{person}/section-preferences` — list preferences
- `PUT /events/{event}/persons/{person}/section-preferences` — replace all preferences
- `GET /organisations/{org}/events/{event}/persons/{person}/section-preferences` — list preferences
- `PUT /organisations/{org}/events/{event}/persons/{person}/section-preferences` — replace all preferences
### Replace Body
@@ -578,7 +578,7 @@ Replaces all field values for this person in one request. Used by both the regis
## Person List Filtering (extended)
Additional filter parameters on `GET /events/{event}/persons`:
Additional filter parameters on `GET /organisations/{org}/events/{event}/persons`:
- `?field[slug]=value` — filter by registration field value (exact match for single-value, `JSON_CONTAINS` for multiselect)
- `?section_preference={section_id}` — filter by section preference (has this section as any priority)