feat(form-builder): controllers and routes (auth + public token)
Phase 5 of S2b. Ten thin controllers plus route registration under the
existing organisations/{organisation} prefix and two unauthenticated
public endpoints.
Controllers (api/app/Http/Controllers/Api/V1/FormBuilder/):
- FormSchemaController: CRUD + duplicate/publish/unpublish/rotate-token/
edit-lock. Returns 410 via PublicFormController when a rotated token is
past its 7-day grace window.
- FormFieldController: CRUD + reorder + insert-from-library. 422 on
binding-change / frozen / cyclic conditional_logic.
- FormSubmissionController: index/store/show/submit/destroy.
- FormValueController: bulk upsert draft values; 403 when
FieldAccessService rejects a write.
- FormSubmissionReviewController, FormSubmissionDelegationController.
- FormTemplateController, FormFieldLibraryController (deactivate on
DELETE for is_active records).
- FormSchemaWebhookController (url/secret never leak — only url_host +
has_secret in responses).
- FilterRegistryController: cached entity_column + tags + form_field
source list for Personen-module (ARCH §7.3–§7.5).
- PublicFormController: GET schema + POST submission. Turnstile captcha
for public_complaint/public_press_request. Rate-limited per
IP+public_token. 410 when token expired.
Routes: grouped under organisations/{organisation}/forms/ for auth'd
routes and public/forms/{public_token}/... with throttle:30,1 for the
public pair. Policies auto-discovered from the namespaced location.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,17 @@ use App\Http\Controllers\Api\V1\Admin\AdminImpersonationController;
|
||||
use App\Http\Controllers\Api\V1\Auth\MfaSetupController;
|
||||
use App\Http\Controllers\Api\V1\Auth\MfaVerifyController;
|
||||
use App\Http\Controllers\Api\V1\Auth\TrustedDeviceController;
|
||||
use App\Http\Controllers\Api\V1\FormBuilder\FilterRegistryController;
|
||||
use App\Http\Controllers\Api\V1\FormBuilder\FormFieldController;
|
||||
use App\Http\Controllers\Api\V1\FormBuilder\FormFieldLibraryController;
|
||||
use App\Http\Controllers\Api\V1\FormBuilder\FormSchemaController;
|
||||
use App\Http\Controllers\Api\V1\FormBuilder\FormSchemaWebhookController;
|
||||
use App\Http\Controllers\Api\V1\FormBuilder\FormSubmissionController;
|
||||
use App\Http\Controllers\Api\V1\FormBuilder\FormSubmissionDelegationController;
|
||||
use App\Http\Controllers\Api\V1\FormBuilder\FormSubmissionReviewController;
|
||||
use App\Http\Controllers\Api\V1\FormBuilder\FormTemplateController;
|
||||
use App\Http\Controllers\Api\V1\FormBuilder\FormValueController;
|
||||
use App\Http\Controllers\Api\V1\FormBuilder\PublicFormController;
|
||||
use App\Models\FestivalSection;
|
||||
use App\Models\Organisation;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
@@ -86,6 +97,12 @@ Route::post('verify-email-change', [EmailChangeController::class, 'verify']);
|
||||
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'])
|
||||
@@ -286,5 +303,63 @@ Route::middleware(['auth:sanctum', 'impersonation'])->group(function () {
|
||||
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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user