response()->json([ 'success' => true, 'message' => 'Crewli API v1', 'timestamp' => now()->toIso8601String(), ])); // Public auth routes Route::post('auth/login', LoginController::class)->middleware('throttle:5,1'); // MFA verification during login (NO auth middleware — uses session token) Route::post('auth/mfa/verify', [MfaVerifyController::class, 'verify'])->middleware('throttle:10,1'); Route::post('auth/mfa/email/send', [MfaVerifyController::class, 'sendEmailCode'])->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::post('public/check-email', CheckEmailController::class)->middleware('throttle:10,1'); Route::post('portal/token-auth', [PortalTokenController::class, 'auth'])->middleware('throttle:10,1'); // Public Form Builder routes (no auth — token-based, rate-limited per ARCH §10) Route::middleware('throttle:30,1')->group(function (): void { Route::get('public/forms/{public_token}', [PublicFormController::class, 'show']); Route::post('public/forms/{public_token}/submissions', [PublicFormController::class, 'submit']); }); // Platform Admin routes Route::prefix('admin') ->middleware(['auth:sanctum', 'impersonation', 'role:super_admin']) ->name('admin.') ->group(function () { // Organisations Route::apiResource('organisations', AdminOrganisationController::class); Route::post('organisations/{organisation}/invite', [AdminOrganisationController::class, 'invite']); Route::put('organisations/{organisation}/members/{user}', [AdminOrganisationController::class, 'updateMemberRole']); Route::delete('organisations/{organisation}/members/{user}', [AdminOrganisationController::class, 'removeMember']); // Users Route::apiResource('users', AdminUserController::class) ->except(['store']); Route::post('users/{user}/reset-mfa', [AdminUserController::class, 'resetMfa']); // Platform statistics Route::get('stats', [AdminStatsController::class, 'index']); // Activity log Route::get('activity-log', [AdminActivityLogController::class, 'index']); // Impersonation — specific routes before wildcard Route::get('impersonate/status', [AdminImpersonationController::class, 'status']); Route::post('impersonate/send-mfa-code', [AdminImpersonationController::class, 'sendMfaCode']); Route::post('impersonate/{user}', [AdminImpersonationController::class, 'start']); }); // Protected routes Route::middleware(['auth:sanctum', 'impersonation'])->group(function () { // Impersonation (stop — accessible by impersonated user, not just super_admin) Route::post('admin/stop-impersonation', [AdminImpersonationController::class, 'stop']); // Auth Route::get('auth/me', MeController::class); Route::post('auth/logout', LogoutController::class); Route::post('auth/refresh', AuthRefreshController::class); // MFA setup and management (authenticated) Route::post('auth/mfa/setup/totp', [MfaSetupController::class, 'setupTotp']); Route::post('auth/mfa/setup/totp/confirm', [MfaSetupController::class, 'confirmTotp']); Route::post('auth/mfa/setup/email', [MfaSetupController::class, 'setupEmail']); Route::post('auth/mfa/setup/email/confirm', [MfaSetupController::class, 'confirmEmail']); Route::post('auth/mfa/disable', [MfaSetupController::class, 'disable']); Route::post('auth/mfa/backup-codes', [MfaSetupController::class, 'regenerateBackupCodes']); Route::get('auth/mfa/status', [MfaSetupController::class, 'status']); Route::put('auth/mfa/preferred-method', [MfaSetupController::class, 'setPreferredMethod']); // Trusted devices Route::get('auth/trusted-devices', [TrustedDeviceController::class, 'index']); Route::delete('auth/trusted-devices/{device}', [TrustedDeviceController::class, 'destroy']); Route::delete('auth/trusted-devices', [TrustedDeviceController::class, 'destroyAll']); // File uploads Route::post('upload/image', [UploadController::class, 'uploadImage']); // Account management (self-service) Route::put('me/profile', [AccountController::class, 'updateProfile']); 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']); Route::get('organisations/{organisation}/dashboard-stats', [OrganisationController::class, 'dashboardStats']); // 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]); }); // Email settings & templates Route::get('email-settings', [OrganisationEmailSettingsController::class, 'show']); Route::put('email-settings', [OrganisationEmailSettingsController::class, 'update']); Route::get('email-templates', [OrganisationEmailTemplateController::class, 'index']); Route::get('email-templates/{type}', [OrganisationEmailTemplateController::class, 'show']); Route::put('email-templates/{type}', [OrganisationEmailTemplateController::class, 'update']); Route::delete('email-templates/{type}', [OrganisationEmailTemplateController::class, 'destroy']); Route::post('email-templates/{type}/preview', [OrganisationEmailTemplateController::class, 'preview']); Route::post('email-templates/{type}/send-test', [OrganisationEmailTemplateController::class, 'sendTest']); // Email logs (read-only) Route::get('email-logs', [EmailLogController::class, 'index']); // Person tags (organisation settings) Route::apiResource('person-tags', PersonTagController::class) ->except(['show']); Route::get('person-tag-categories', [PersonTagController::class, 'categories']); // 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']); // 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']); }); // Form Builder (ARCH-FORM-BUILDER.md) Route::prefix('forms')->group(function (): void { // Filter registry Route::get('filter-registry', [FilterRegistryController::class, 'show']); // Form schemas Route::get('schemas', [FormSchemaController::class, 'index']); Route::post('schemas', [FormSchemaController::class, 'store']); Route::get('schemas/{form_schema}', [FormSchemaController::class, 'show']); Route::put('schemas/{form_schema}', [FormSchemaController::class, 'update']); Route::delete('schemas/{form_schema}', [FormSchemaController::class, 'destroy']); Route::post('schemas/{form_schema}/duplicate', [FormSchemaController::class, 'duplicate']); Route::post('schemas/{form_schema}/publish', [FormSchemaController::class, 'publish']); Route::post('schemas/{form_schema}/unpublish', [FormSchemaController::class, 'unpublish']); Route::post('schemas/{form_schema}/rotate-public-token', [FormSchemaController::class, 'rotatePublicToken']); Route::post('schemas/{form_schema}/edit-lock', [FormSchemaController::class, 'acquireEditLock']); Route::delete('schemas/{form_schema}/edit-lock', [FormSchemaController::class, 'releaseEditLock']); // Form fields (scoped under schema) Route::get('schemas/{form_schema}/fields', [FormFieldController::class, 'index']); Route::post('schemas/{form_schema}/fields', [FormFieldController::class, 'store']); Route::post('schemas/{form_schema}/fields/reorder', [FormFieldController::class, 'reorder']); Route::post('schemas/{form_schema}/fields/insert-from-library', [FormFieldController::class, 'insertFromLibrary']); Route::put('schemas/{form_schema}/fields/{form_field}', [FormFieldController::class, 'update']); Route::delete('schemas/{form_schema}/fields/{form_field}', [FormFieldController::class, 'destroy']); // Form submissions (list under schema, item under /forms/submissions) Route::get('schemas/{form_schema}/submissions', [FormSubmissionController::class, 'index']); Route::post('schemas/{form_schema}/submissions', [FormSubmissionController::class, 'store']); Route::get('submissions/{form_submission}', [FormSubmissionController::class, 'show']); Route::put('submissions/{form_submission}/field-values', [FormValueController::class, 'upsert']); Route::post('submissions/{form_submission}/submit', [FormSubmissionController::class, 'submit']); Route::post('submissions/{form_submission}/review', [FormSubmissionReviewController::class, 'review']); Route::post('submissions/{form_submission}/delegate', [FormSubmissionDelegationController::class, 'delegate']); Route::delete('submissions/{form_submission}/delegations/{delegation}', [FormSubmissionDelegationController::class, 'revoke']); Route::delete('submissions/{form_submission}', [FormSubmissionController::class, 'destroy']); // Form templates Route::get('templates', [FormTemplateController::class, 'index']); Route::post('templates', [FormTemplateController::class, 'store']); Route::get('templates/{form_template}', [FormTemplateController::class, 'show']); Route::put('templates/{form_template}', [FormTemplateController::class, 'update']); Route::delete('templates/{form_template}', [FormTemplateController::class, 'destroy']); // Form field library Route::get('field-library', [FormFieldLibraryController::class, 'index']); Route::post('field-library', [FormFieldLibraryController::class, 'store']); Route::get('field-library/{field_library}', [FormFieldLibraryController::class, 'show']); Route::put('field-library/{field_library}', [FormFieldLibraryController::class, 'update']); Route::delete('field-library/{field_library}', [FormFieldLibraryController::class, 'destroy']); // Schema webhooks Route::get('schemas/{form_schema}/webhooks', [FormSchemaWebhookController::class, 'index']); Route::post('schemas/{form_schema}/webhooks', [FormSchemaWebhookController::class, 'store']); Route::put('schemas/{form_schema}/webhooks/{webhook}', [FormSchemaWebhookController::class, 'update']); Route::delete('schemas/{form_schema}/webhooks/{webhook}', [FormSchemaWebhookController::class, 'destroy']); }); }); });