From f0e8cee8502b0f855799519feec67315f049f010 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Wed, 28 Jan 2026 20:46:12 +0100 Subject: [PATCH] Expand support API validation for writable resources --- .../SupportBlogPostResourceRequest.php | 71 ++++++++++++++++++ .../SupportEmotionResourceRequest.php | 46 ++++++++++++ .../Resources/SupportEventResourceRequest.php | 74 +++++++++++++++++++ .../Resources/SupportPhotoResourceRequest.php | 31 ++++++++ .../Resources/SupportTaskResourceRequest.php | 52 +++++++++++++ config/support-api.php | 62 +++++++++++----- tests/Feature/Support/SupportApiTest.php | 48 ++++++++++++ 7 files changed, 367 insertions(+), 17 deletions(-) create mode 100644 app/Http/Requests/Support/Resources/SupportBlogPostResourceRequest.php create mode 100644 app/Http/Requests/Support/Resources/SupportEmotionResourceRequest.php create mode 100644 app/Http/Requests/Support/Resources/SupportEventResourceRequest.php create mode 100644 app/Http/Requests/Support/Resources/SupportPhotoResourceRequest.php create mode 100644 app/Http/Requests/Support/Resources/SupportTaskResourceRequest.php diff --git a/app/Http/Requests/Support/Resources/SupportBlogPostResourceRequest.php b/app/Http/Requests/Support/Resources/SupportBlogPostResourceRequest.php new file mode 100644 index 0000000..ac19e3b --- /dev/null +++ b/app/Http/Requests/Support/Resources/SupportBlogPostResourceRequest.php @@ -0,0 +1,71 @@ +getKey(); + + $rules = [ + 'blog_category_id' => ['sometimes', 'integer', 'exists:blog_categories,id'], + 'slug' => [ + 'sometimes', + 'string', + 'max:255', + Rule::unique('blog_posts', 'slug')->ignore($postId), + ], + 'banner' => ['sometimes', 'nullable', 'string', 'max:255'], + 'published_at' => ['sometimes', 'nullable', 'date'], + 'is_published' => ['sometimes', 'boolean'], + 'title' => ['sometimes', 'array'], + 'title.de' => ['required_with:title', 'string', 'max:255'], + 'title.en' => ['nullable', 'string', 'max:255'], + 'content' => ['sometimes', 'array'], + 'content.de' => ['required_with:content', 'string'], + 'content.en' => ['nullable', 'string'], + 'excerpt' => ['sometimes', 'array'], + 'excerpt.de' => ['nullable', 'string'], + 'excerpt.en' => ['nullable', 'string'], + 'meta_title' => ['sometimes', 'array'], + 'meta_title.de' => ['nullable', 'string', 'max:255'], + 'meta_title.en' => ['nullable', 'string', 'max:255'], + 'meta_description' => ['sometimes', 'array'], + 'meta_description.de' => ['nullable', 'string'], + 'meta_description.en' => ['nullable', 'string'], + 'translations' => ['sometimes', 'array'], + ]; + + if ($action === 'create') { + $rules['blog_category_id'] = ['required', 'integer', 'exists:blog_categories,id']; + $rules['slug'] = ['required', 'string', 'max:255', Rule::unique('blog_posts', 'slug')]; + $rules['title'] = ['required', 'array']; + $rules['title.de'] = ['required', 'string', 'max:255']; + $rules['content'] = ['required', 'array']; + $rules['content.de'] = ['required', 'string']; + } + + return $rules; + } + + public static function allowedFields(string $action): array + { + return [ + 'blog_category_id', + 'slug', + 'banner', + 'published_at', + 'is_published', + 'title', + 'content', + 'excerpt', + 'meta_title', + 'meta_description', + 'translations', + ]; + } +} diff --git a/app/Http/Requests/Support/Resources/SupportEmotionResourceRequest.php b/app/Http/Requests/Support/Resources/SupportEmotionResourceRequest.php new file mode 100644 index 0000000..b0905e3 --- /dev/null +++ b/app/Http/Requests/Support/Resources/SupportEmotionResourceRequest.php @@ -0,0 +1,46 @@ + ['sometimes', 'array'], + 'name.de' => ['required_with:name', 'string', 'max:255'], + 'name.en' => ['required_with:name', 'string', 'max:255'], + 'description' => ['sometimes', 'array'], + 'description.de' => ['nullable', 'string'], + 'description.en' => ['nullable', 'string'], + 'icon' => ['sometimes', 'nullable', 'string', 'max:50'], + 'color' => ['sometimes', 'nullable', 'string', 'max:7'], + 'sort_order' => ['sometimes', 'integer', 'min:0'], + 'is_active' => ['sometimes', 'boolean'], + 'tenant_id' => ['sometimes', 'nullable', 'integer', 'exists:tenants,id'], + ]; + + if ($action === 'create') { + $rules['name'] = ['required', 'array']; + $rules['name.de'] = ['required', 'string', 'max:255']; + $rules['name.en'] = ['required', 'string', 'max:255']; + } + + return $rules; + } + + public static function allowedFields(string $action): array + { + return [ + 'name', + 'description', + 'icon', + 'color', + 'sort_order', + 'is_active', + 'tenant_id', + ]; + } +} diff --git a/app/Http/Requests/Support/Resources/SupportEventResourceRequest.php b/app/Http/Requests/Support/Resources/SupportEventResourceRequest.php new file mode 100644 index 0000000..5c331a2 --- /dev/null +++ b/app/Http/Requests/Support/Resources/SupportEventResourceRequest.php @@ -0,0 +1,74 @@ +getKey(); + + $rules = [ + 'name' => ['sometimes', 'array'], + 'name.de' => ['required_with:name', 'string', 'max:255'], + 'name.en' => ['nullable', 'string', 'max:255'], + 'description' => ['sometimes', 'array'], + 'description.de' => ['nullable', 'string'], + 'description.en' => ['nullable', 'string'], + 'slug' => [ + 'sometimes', + 'string', + 'max:255', + Rule::unique('events', 'slug')->ignore($eventId), + ], + 'date' => ['sometimes', 'date'], + 'location' => ['sometimes', 'nullable', 'string', 'max:255'], + 'max_participants' => ['sometimes', 'nullable', 'integer', 'min:0'], + 'event_type_id' => ['sometimes', 'nullable', 'integer', 'exists:event_types,id'], + 'default_locale' => ['sometimes', 'string', 'max:5'], + 'is_active' => ['sometimes', 'boolean'], + 'status' => ['sometimes', 'string', Rule::in(['draft', 'published', 'archived'])], + 'settings' => ['sometimes', 'array'], + 'join_link_enabled' => ['sometimes', 'boolean'], + 'photo_upload_enabled' => ['sometimes', 'boolean'], + 'task_checklist_enabled' => ['sometimes', 'boolean'], + ]; + + if ($action === 'create') { + $rules['name'] = ['required', 'array']; + $rules['name.de'] = ['required', 'string', 'max:255']; + $rules['slug'] = [ + 'required', + 'string', + 'max:255', + Rule::unique('events', 'slug'), + ]; + $rules['date'] = ['required', 'date']; + } + + return $rules; + } + + public static function allowedFields(string $action): array + { + return [ + 'name', + 'description', + 'slug', + 'date', + 'location', + 'max_participants', + 'event_type_id', + 'default_locale', + 'is_active', + 'status', + 'settings', + 'join_link_enabled', + 'photo_upload_enabled', + 'task_checklist_enabled', + ]; + } +} diff --git a/app/Http/Requests/Support/Resources/SupportPhotoResourceRequest.php b/app/Http/Requests/Support/Resources/SupportPhotoResourceRequest.php new file mode 100644 index 0000000..1bb53c6 --- /dev/null +++ b/app/Http/Requests/Support/Resources/SupportPhotoResourceRequest.php @@ -0,0 +1,31 @@ + ['sometimes', 'string', Rule::in(['pending', 'approved', 'rejected', 'hidden'])], + 'moderation_notes' => ['nullable', 'required_if:status,rejected', 'string', 'max:1000'], + 'is_featured' => ['sometimes', 'boolean'], + 'emotion_id' => ['sometimes', 'nullable', 'integer', 'exists:emotions,id'], + 'task_id' => ['sometimes', 'nullable', 'integer', 'exists:tasks,id'], + ]; + } + + public static function allowedFields(string $action): array + { + return [ + 'status', + 'moderation_notes', + 'is_featured', + 'emotion_id', + 'task_id', + ]; + } +} diff --git a/app/Http/Requests/Support/Resources/SupportTaskResourceRequest.php b/app/Http/Requests/Support/Resources/SupportTaskResourceRequest.php new file mode 100644 index 0000000..8dbc7d9 --- /dev/null +++ b/app/Http/Requests/Support/Resources/SupportTaskResourceRequest.php @@ -0,0 +1,52 @@ + ['sometimes', 'integer', 'exists:emotions,id'], + 'event_type_id' => ['sometimes', 'nullable', 'integer', 'exists:event_types,id'], + 'title' => ['sometimes', 'array'], + 'title.de' => ['required_with:title', 'string', 'max:255'], + 'title.en' => ['required_with:title', 'string', 'max:255'], + 'description' => ['sometimes', 'array'], + 'description.de' => ['nullable', 'string'], + 'description.en' => ['nullable', 'string'], + 'example_text' => ['sometimes', 'array'], + 'example_text.de' => ['nullable', 'string'], + 'example_text.en' => ['nullable', 'string'], + 'difficulty' => ['sometimes', 'string', Rule::in(['easy', 'medium', 'hard'])], + 'sort_order' => ['sometimes', 'integer', 'min:0'], + 'is_active' => ['sometimes', 'boolean'], + ]; + + if ($action === 'create') { + $rules['emotion_id'] = ['required', 'integer', 'exists:emotions,id']; + $rules['title'] = ['required', 'array']; + $rules['title.de'] = ['required', 'string', 'max:255']; + $rules['title.en'] = ['required', 'string', 'max:255']; + } + + return $rules; + } + + public static function allowedFields(string $action): array + { + return [ + 'emotion_id', + 'event_type_id', + 'title', + 'description', + 'example_text', + 'difficulty', + 'sort_order', + 'is_active', + ]; + } +} diff --git a/config/support-api.php b/config/support-api.php index 3a42735..8ce907f 100644 --- a/config/support-api.php +++ b/config/support-api.php @@ -1,7 +1,12 @@ [ 'model' => Event::class, 'search' => ['name', 'slug'], - 'read_only' => true, 'abilities' => [ 'read' => ['support:read'], + 'write' => ['support:write'], + ], + 'validation' => [ + 'update' => SupportEventResourceRequest::class, ], 'mutations' => [ 'create' => false, - 'update' => false, - 'delete' => false, + 'update' => true, + 'delete' => true, ], ], 'event-types' => [ @@ -116,9 +124,17 @@ return [ 'photos' => [ 'model' => Photo::class, 'search' => ['id'], - 'read_only' => true, 'abilities' => [ 'read' => ['support:read'], + 'write' => ['support:write'], + ], + 'validation' => [ + 'update' => SupportPhotoResourceRequest::class, + ], + 'mutations' => [ + 'create' => false, + 'update' => true, + 'delete' => true, ], ], 'event-purchases' => [ @@ -321,40 +337,52 @@ return [ 'blog-posts' => [ 'model' => BlogPost::class, 'search' => ['title', 'slug'], - 'read_only' => true, 'abilities' => [ 'read' => ['support:content'], + 'write' => ['support:content'], + ], + 'validation' => [ + 'create' => SupportBlogPostResourceRequest::class, + 'update' => SupportBlogPostResourceRequest::class, ], 'mutations' => [ - 'create' => false, - 'update' => false, - 'delete' => false, + 'create' => true, + 'update' => true, + 'delete' => true, ], ], 'emotions' => [ 'model' => Emotion::class, 'search' => ['name', 'slug'], - 'read_only' => true, 'abilities' => [ 'read' => ['support:content'], + 'write' => ['support:content'], + ], + 'validation' => [ + 'create' => SupportEmotionResourceRequest::class, + 'update' => SupportEmotionResourceRequest::class, ], 'mutations' => [ - 'create' => false, - 'update' => false, - 'delete' => false, + 'create' => true, + 'update' => true, + 'delete' => true, ], ], 'tasks' => [ 'model' => Task::class, 'search' => ['title'], - 'read_only' => true, 'abilities' => [ - 'read' => ['support:read'], + 'read' => ['support:content'], + 'write' => ['support:content'], + ], + 'validation' => [ + 'create' => SupportTaskResourceRequest::class, + 'update' => SupportTaskResourceRequest::class, ], 'mutations' => [ - 'create' => false, - 'update' => false, - 'delete' => false, + 'create' => true, + 'update' => true, + 'delete' => true, ], ], 'task-collections' => [ diff --git a/tests/Feature/Support/SupportApiTest.php b/tests/Feature/Support/SupportApiTest.php index f47fa34..04ec971 100644 --- a/tests/Feature/Support/SupportApiTest.php +++ b/tests/Feature/Support/SupportApiTest.php @@ -2,6 +2,8 @@ namespace Tests\Feature\Support; +use App\Models\BlogCategory; +use App\Models\Photo; use App\Models\Tenant; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -82,4 +84,50 @@ class SupportApiTest extends TestCase Bus::assertDispatched(\App\Jobs\GenerateDataExport::class); } + + public function test_support_photo_reject_requires_moderation_notes(): void + { + $user = User::factory()->create([ + 'role' => 'super_admin', + ]); + + $photo = Photo::factory()->create(); + + Sanctum::actingAs($user, ['support-admin', 'support:write']); + + $response = $this->patchJson('/api/v1/support/photos/'.$photo->id, [ + 'data' => [ + 'status' => 'rejected', + ], + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['moderation_notes']); + } + + public function test_support_blog_post_create_requires_title_and_content(): void + { + $user = User::factory()->create([ + 'role' => 'super_admin', + ]); + + $category = BlogCategory::create([ + 'slug' => 'news', + 'name' => ['de' => 'News', 'en' => 'News'], + 'is_visible' => true, + ]); + + Sanctum::actingAs($user, ['support-admin', 'support:content']); + + $response = $this->postJson('/api/v1/support/blog-posts', [ + 'data' => [ + 'blog_category_id' => $category->id, + 'slug' => 'missing-title', + 'is_published' => false, + ], + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['title', 'content']); + } }