response()->json([ 'success' => true, 'message' => 'Crewli API v1', 'timestamp' => now()->toIso8601String(), ])); // Public auth routes Route::post('auth/login', LoginController::class)->middleware('throttle:5,1'); // Public invitation routes (no auth required) Route::get('invitations/{token}', [InvitationController::class, 'show'])->middleware('throttle:10,1'); Route::post('invitations/{token}/accept', [InvitationController::class, 'accept'])->middleware('throttle:10,1'); // Password reset Route::post('auth/forgot-password', [PasswordResetController::class, 'sendResetLink']) ->middleware('throttle:5,1'); Route::post('auth/reset-password', [PasswordResetController::class, 'resetPassword']); // Email change verification (public — token from email link) Route::post('verify-email-change', [EmailChangeController::class, 'verify']); // Public portal routes Route::get('public/events/{slug}/registration-data', PublicRegistrationDataController::class); Route::post('public/check-email', CheckEmailController::class)->middleware('throttle:10,1'); Route::post('events/{event}/volunteer-register', VolunteerRegistrationController::class)->middleware('throttle:5,1'); Route::post('portal/token-auth', [PortalTokenController::class, 'auth'])->middleware('throttle:10,1'); // Protected routes Route::middleware('auth:sanctum')->group(function () { // Auth Route::get('auth/me', MeController::class); Route::post('auth/logout', LogoutController::class); Route::post('auth/refresh', AuthRefreshController::class); // Account management (self-service) Route::post('me/change-password', [AccountController::class, 'changePassword']); Route::post('me/change-email', [EmailChangeController::class, 'request']); // Portal (authenticated) Route::get('portal/me', [PortalMeController::class, 'index']); Route::put('portal/profile', [PortalMeController::class, 'updateProfile']); Route::put('portal/password', [PortalMeController::class, 'updatePassword']); Route::get('portal/my-shifts', [PortalShiftController::class, 'allMyShifts']); Route::get('portal/events/{event}/available-shifts', [PortalShiftController::class, 'availableShifts']); Route::get('portal/events/{event}/my-shifts', [PortalShiftController::class, 'myShifts']); Route::post('portal/events/{event}/shifts/{shift}/claim', [PortalShiftController::class, 'claim']); Route::post('portal/events/{event}/assignments/{shiftAssignment}/cancel', [PortalShiftController::class, 'cancel']); // Organisations Route::apiResource('organisations', OrganisationController::class) ->only(['index', 'show', 'store', 'update']); // Events (nested under organisations) Route::apiResource('organisations.events', EventController::class) ->only(['index', 'show', 'store', 'update', 'destroy']); Route::get('organisations/{organisation}/events/{event}/children', [EventController::class, 'children']); Route::post('organisations/{organisation}/events/{event}/transition', [EventController::class, 'transition']); Route::post('organisations/{organisation}/events/{event}/upload-image', [EventController::class, 'uploadImage']); // Organisation-scoped resources Route::prefix('organisations/{organisation}')->group(function () { Route::apiResource('crowd-types', CrowdTypeController::class) ->except(['show']); Route::apiResource('companies', CompanyController::class); // Section categories (autocomplete) Route::get('section-categories', function (Organisation $organisation) { Gate::authorize('view', $organisation); $categories = FestivalSection::query() ->whereIn('event_id', $organisation->events()->select('id')) ->whereNotNull('category') ->distinct() ->orderBy('category') ->pluck('category'); return response()->json(['data' => $categories]); }); // Person tags (organisation settings) Route::apiResource('person-tags', PersonTagController::class) ->except(['show']); Route::get('person-tag-categories', [PersonTagController::class, 'categories']); // Registration field templates (organisation settings) Route::apiResource('registration-field-templates', RegistrationFieldTemplateController::class) ->except(['show']); // User tag assignments Route::get('users/{user}/tags', [UserOrganisationTagController::class, 'index']); Route::post('users/{user}/tags', [UserOrganisationTagController::class, 'store']); Route::put('users/{user}/tags/sync', [UserOrganisationTagController::class, 'sync']); Route::delete('users/{user}/tags/{userOrganisationTag}', [UserOrganisationTagController::class, 'destroy']); // Identity matches Route::get('identity-matches', [PersonIdentityMatchController::class, 'index']); Route::get('persons/{person}/identity-match', [PersonIdentityMatchController::class, 'showForPerson']); Route::post('identity-matches/{personIdentityMatch}/confirm', [PersonIdentityMatchController::class, 'confirm']); Route::post('identity-matches/{personIdentityMatch}/dismiss', [PersonIdentityMatchController::class, 'dismiss']); Route::post('identity-matches/{personIdentityMatch}/revert', [PersonIdentityMatchController::class, 'revert']); Route::post('identity-matches/bulk-confirm', [PersonIdentityMatchController::class, 'bulkConfirm']); // Invitations & Members Route::post('invite', [InvitationController::class, 'invite']); Route::delete('invitations/{invitation}', [InvitationController::class, 'revoke']); Route::get('members', [MemberController::class, 'index']); Route::put('members/{user}', [MemberController::class, 'update']); Route::delete('members/{user}', [MemberController::class, 'destroy']); Route::post('members/{user}/change-email', [MemberController::class, 'changeEmail']); Route::get('members/available-for-event/{event}', [MemberController::class, 'availableForEvent']); // Event sub-resources (all nested under organisation prefix — A01-13) Route::prefix('events/{event}')->group(function () { // Stats Route::get('stats', [EventController::class, 'stats']); // Locations Route::apiResource('locations', LocationController::class) ->except(['show']); // Festival sections Route::get('sections/registration-settings', [FestivalSectionController::class, 'registrationSettings']); Route::put('sections/registration-settings', [FestivalSectionController::class, 'updateRegistrationSettings']); Route::apiResource('sections', FestivalSectionController::class) ->except(['show']); Route::post('sections/reorder', [FestivalSectionController::class, 'reorder']); // Time slots Route::apiResource('time-slots', TimeSlotController::class) ->except(['show']); // Shifts nested under sections Route::prefix('sections/{section}')->group(function () { Route::apiResource('shifts', ShiftController::class) ->except(['show']); Route::post('shifts/{shift}/assign', [ShiftController::class, 'assign']); Route::post('shifts/{shift}/claim', [ShiftController::class, 'claim']); }); // Shift assignments (event-level) Route::get('shift-assignments', [ShiftAssignmentController::class, 'index']); Route::get('shifts/{shift}/assignable-persons', [ShiftAssignmentController::class, 'assignablePersons']); Route::post('shift-assignments/{shiftAssignment}/approve', [ShiftAssignmentController::class, 'approve']); Route::post('shift-assignments/{shiftAssignment}/reject', [ShiftAssignmentController::class, 'reject']); Route::post('shift-assignments/{shiftAssignment}/cancel', [ShiftAssignmentController::class, 'cancel']); Route::post('shift-assignments/bulk-approve', [ShiftAssignmentController::class, 'bulkApprove']); // Persons Route::apiResource('persons', PersonController::class); Route::post('persons/from-member', [PersonController::class, 'createFromMember']); Route::post('persons/{person}/approve', [PersonController::class, 'approve']); Route::post('persons/{person}/reject', [PersonController::class, 'reject']); Route::post('persons/{person}/manual-link', [PersonIdentityMatchController::class, 'manualLink']); Route::post('persons/{person}/unlink', [PersonIdentityMatchController::class, 'unlink']); // Volunteer availabilities Route::get('persons/{person}/availabilities', [VolunteerAvailabilityController::class, 'index']); Route::post('persons/{person}/availabilities/sync', [VolunteerAvailabilityController::class, 'sync']); // Person field values Route::get('persons/{person}/field-values', [PersonFieldValueController::class, 'index']); Route::put('persons/{person}/field-values', [PersonFieldValueController::class, 'upsert']); // Person section preferences Route::get('persons/{person}/section-preferences', [PersonSectionPreferenceController::class, 'index']); Route::put('persons/{person}/section-preferences', [PersonSectionPreferenceController::class, 'replace']); // Registration form fields (event settings) Route::apiResource('registration-fields', RegistrationFormFieldController::class) ->except(['show']); Route::post('registration-fields/reorder', [RegistrationFormFieldController::class, 'reorder']); Route::post('registration-fields/from-template', [RegistrationFormFieldController::class, 'fromTemplate']); Route::post('registration-fields/import-from-event', [RegistrationFormFieldController::class, 'importFromEvent']); // Crowd lists Route::apiResource('crowd-lists', CrowdListController::class) ->except(['show']); Route::get('crowd-lists/{crowdList}/persons', [CrowdListController::class, 'persons']); Route::post('crowd-lists/{crowdList}/persons', [CrowdListController::class, 'addPerson']); Route::delete('crowd-lists/{crowdList}/persons/{person}', [CrowdListController::class, 'removePerson']); }); }); });