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:
126
dev-docs/API.md
126
dev-docs/API.md
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user