Add support API validation rules
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-28 19:42:28 +01:00
parent 6bc1d86009
commit 981df2ee45
10 changed files with 372 additions and 3 deletions

View File

@@ -2,8 +2,12 @@
namespace App\Http\Controllers\Api\Support;
use App\Enums\DataExportScope;
use App\Http\Controllers\Controller;
use App\Http\Requests\Support\Resources\SupportResourceFormRequest;
use App\Http\Requests\Support\SupportResourceRequest;
use App\Jobs\GenerateDataExport;
use App\Models\DataExport;
use App\Support\ApiError;
use App\Support\SupportApiAuthorizer;
use App\Support\SupportApiRegistry;
@@ -13,6 +17,7 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Validator;
class SupportResourceController extends Controller
{
@@ -89,14 +94,26 @@ class SupportResourceController extends Controller
/** @var Model $model */
$model = new $modelClass;
$payload = $this->filteredPayload($request, $model);
$payload = $this->validatedPayload($request, $resource, 'create', $model);
if ($payload instanceof JsonResponse) {
return $payload;
}
if ($payload === []) {
return $this->emptyPayloadResponse($resource);
}
if ($resource === 'data-exports') {
$payload = $this->normalizeDataExportPayload($request, $payload);
}
$record = $modelClass::query()->create($payload);
if ($resource === 'data-exports') {
GenerateDataExport::dispatch($record->id);
}
return response()->json([
'data' => $record,
], 201);
@@ -118,7 +135,11 @@ class SupportResourceController extends Controller
return $this->resourceNotFoundResponse($resource, $record);
}
$payload = $this->filteredPayload($request, $model);
$payload = $this->validatedPayload($request, $resource, 'update', $model);
if ($payload instanceof JsonResponse) {
return $payload;
}
if ($payload === []) {
return $this->emptyPayloadResponse($resource);
@@ -174,7 +195,7 @@ class SupportResourceController extends Controller
return $query->where($keyName, $record)->first();
}
private function filteredPayload(SupportResourceRequest $request, Model $model): array
private function validatedPayload(SupportResourceRequest $request, string $resource, string $action, Model $model): array|JsonResponse
{
$payload = $request->validated('data');
@@ -182,6 +203,28 @@ class SupportResourceController extends Controller
return [];
}
$validationClass = SupportApiRegistry::validationClass($resource, $action);
if ($validationClass && is_subclass_of($validationClass, SupportResourceFormRequest::class)) {
$allowedFields = $validationClass::allowedFields($action);
if ($allowedFields !== []) {
$unexpected = array_diff(array_keys($payload), $allowedFields);
if ($unexpected !== []) {
return $this->invalidFieldResponse($resource, $unexpected);
}
}
$rules = $validationClass::rulesFor($action, $model);
if ($rules !== []) {
$payload = Validator::make($payload, $rules)->validate();
}
if ($allowedFields !== []) {
$payload = Arr::only($payload, $allowedFields);
}
}
$fillable = $model->getFillable();
if ($fillable === [] && method_exists($model, 'getGuarded') && $model->getGuarded() !== ['*']) {
@@ -292,6 +335,19 @@ class SupportResourceController extends Controller
);
}
private function invalidFieldResponse(string $resource, array $fields): JsonResponse
{
return ApiError::response(
'support_invalid_fields',
'Invalid Fields',
"Unsupported fields provided for {$resource}.",
422,
[
'fields' => array_values($fields),
]
);
}
private function resourceNotFoundResponse(string $resource, ?string $record = null): JsonResponse
{
$message = $record
@@ -305,4 +361,16 @@ class SupportResourceController extends Controller
404
);
}
private function normalizeDataExportPayload(Request $request, array $payload): array
{
$payload['user_id'] = $request->user()?->id;
$payload['status'] = DataExport::STATUS_PENDING;
if (($payload['scope'] ?? null) !== DataExportScope::EVENT->value) {
$payload['event_id'] = null;
}
return $payload;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests\Support\Resources;
use App\Enums\DataExportScope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\Rule;
class SupportDataExportResourceRequest extends SupportResourceFormRequest
{
public static function rulesFor(string $action, ?Model $model = null): array
{
$scopeValues = array_map(static fn (DataExportScope $scope): string => $scope->value, DataExportScope::cases());
return [
'scope' => ['required', 'string', Rule::in($scopeValues)],
'tenant_id' => ['required', 'integer', 'exists:tenants,id'],
'event_id' => ['nullable', 'integer', 'exists:events,id', 'required_if:scope,event'],
'include_media' => ['sometimes', 'boolean'],
];
}
public static function allowedFields(string $action): array
{
return [
'scope',
'tenant_id',
'event_id',
'include_media',
];
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests\Support\Resources;
use Illuminate\Database\Eloquent\Model;
class SupportPhotoboothSettingResourceRequest extends SupportResourceFormRequest
{
public static function rulesFor(string $action, ?Model $model = null): array
{
return [
'ftp_port' => ['sometimes', 'integer', 'min:1', 'max:65535'],
'rate_limit_per_minute' => ['sometimes', 'integer', 'min:1', 'max:200'],
'expiry_grace_days' => ['sometimes', 'integer', 'min:0', 'max:14'],
'require_ftps' => ['sometimes', 'boolean'],
'allowed_ip_ranges' => ['sometimes', 'array'],
'control_service_base_url' => ['sometimes', 'nullable', 'string', 'max:191'],
'control_service_token_identifier' => ['sometimes', 'nullable', 'string', 'max:191'],
];
}
public static function allowedFields(string $action): array
{
return [
'ftp_port',
'rate_limit_per_minute',
'expiry_grace_days',
'require_ftps',
'allowed_ip_ranges',
'control_service_base_url',
'control_service_token_identifier',
];
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Requests\Support\Resources;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Http\FormRequest;
abstract class SupportResourceFormRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, mixed>
*/
public static function rulesFor(string $action, ?Model $model = null): array
{
return [];
}
/**
* @return array<int, string>
*/
public static function allowedFields(string $action): array
{
return [];
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return static::rulesFor('update', null);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Support\Resources;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\Rule;
class SupportTenantFeedbackResourceRequest extends SupportResourceFormRequest
{
public static function rulesFor(string $action, ?Model $model = null): array
{
return [
'status' => [
'sometimes',
'string',
Rule::in(['pending', 'resolved', 'hidden', 'deleted']),
],
'moderation_notes' => ['sometimes', 'nullable', 'string', 'max:1000'],
];
}
public static function allowedFields(string $action): array
{
return [
'status',
'moderation_notes',
];
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Http\Requests\Support\Resources;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\Rule;
class SupportTenantResourceRequest extends SupportResourceFormRequest
{
public static function rulesFor(string $action, ?Model $model = null): array
{
$tenantId = $model?->getKey();
return [
'slug' => [
'sometimes',
'string',
'max:255',
Rule::unique('tenants', 'slug')->ignore($tenantId),
],
'contact_email' => ['sometimes', 'email', 'max:255'],
'paddle_customer_id' => ['sometimes', 'nullable', 'string', 'max:191'],
'is_active' => ['sometimes', 'boolean'],
'is_suspended' => ['sometimes', 'boolean'],
'features' => ['sometimes', 'array'],
];
}
public static function allowedFields(string $action): array
{
return [
'slug',
'contact_email',
'paddle_customer_id',
'is_active',
'is_suspended',
'features',
];
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Http\Requests\Support\Resources;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\Rule;
class SupportUserResourceRequest extends SupportResourceFormRequest
{
public static function rulesFor(string $action, ?Model $model = null): array
{
$userId = $model?->getKey();
return [
'first_name' => ['sometimes', 'string', 'max:255'],
'last_name' => ['sometimes', 'string', 'max:255'],
'username' => [
'sometimes',
'string',
'max:255',
Rule::unique('users', 'username')->ignore($userId),
],
'email' => [
'sometimes',
'email',
'max:255',
Rule::unique('users', 'email')->ignore($userId),
],
'address' => ['sometimes', 'string', 'max:1000'],
'phone' => ['sometimes', 'string', 'max:50'],
'preferred_locale' => ['sometimes', 'string', 'max:10'],
];
}
public static function allowedFields(string $action): array
{
return [
'first_name',
'last_name',
'username',
'email',
'address',
'phone',
'preferred_locale',
];
}
}

View File

@@ -22,6 +22,19 @@ class SupportApiRegistry
return $resources[$resource] ?? null;
}
public static function validationClass(string $resource, string $action): ?string
{
$config = self::get($resource);
if (! $config) {
return null;
}
$validation = $config['validation'][$action] ?? null;
return is_string($validation) ? $validation : null;
}
/**
* @return array<int, string>
*/