Add support API validation rules
This commit is contained in:
@@ -2,8 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api\Support;
|
namespace App\Http\Controllers\Api\Support;
|
||||||
|
|
||||||
|
use App\Enums\DataExportScope;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Support\Resources\SupportResourceFormRequest;
|
||||||
use App\Http\Requests\Support\SupportResourceRequest;
|
use App\Http\Requests\Support\SupportResourceRequest;
|
||||||
|
use App\Jobs\GenerateDataExport;
|
||||||
|
use App\Models\DataExport;
|
||||||
use App\Support\ApiError;
|
use App\Support\ApiError;
|
||||||
use App\Support\SupportApiAuthorizer;
|
use App\Support\SupportApiAuthorizer;
|
||||||
use App\Support\SupportApiRegistry;
|
use App\Support\SupportApiRegistry;
|
||||||
@@ -13,6 +17,7 @@ use Illuminate\Http\JsonResponse;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
class SupportResourceController extends Controller
|
class SupportResourceController extends Controller
|
||||||
{
|
{
|
||||||
@@ -89,14 +94,26 @@ class SupportResourceController extends Controller
|
|||||||
/** @var Model $model */
|
/** @var Model $model */
|
||||||
$model = new $modelClass;
|
$model = new $modelClass;
|
||||||
|
|
||||||
$payload = $this->filteredPayload($request, $model);
|
$payload = $this->validatedPayload($request, $resource, 'create', $model);
|
||||||
|
|
||||||
|
if ($payload instanceof JsonResponse) {
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
if ($payload === []) {
|
if ($payload === []) {
|
||||||
return $this->emptyPayloadResponse($resource);
|
return $this->emptyPayloadResponse($resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($resource === 'data-exports') {
|
||||||
|
$payload = $this->normalizeDataExportPayload($request, $payload);
|
||||||
|
}
|
||||||
|
|
||||||
$record = $modelClass::query()->create($payload);
|
$record = $modelClass::query()->create($payload);
|
||||||
|
|
||||||
|
if ($resource === 'data-exports') {
|
||||||
|
GenerateDataExport::dispatch($record->id);
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'data' => $record,
|
'data' => $record,
|
||||||
], 201);
|
], 201);
|
||||||
@@ -118,7 +135,11 @@ class SupportResourceController extends Controller
|
|||||||
return $this->resourceNotFoundResponse($resource, $record);
|
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 === []) {
|
if ($payload === []) {
|
||||||
return $this->emptyPayloadResponse($resource);
|
return $this->emptyPayloadResponse($resource);
|
||||||
@@ -174,7 +195,7 @@ class SupportResourceController extends Controller
|
|||||||
return $query->where($keyName, $record)->first();
|
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');
|
$payload = $request->validated('data');
|
||||||
|
|
||||||
@@ -182,6 +203,28 @@ class SupportResourceController extends Controller
|
|||||||
return [];
|
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();
|
$fillable = $model->getFillable();
|
||||||
|
|
||||||
if ($fillable === [] && method_exists($model, 'getGuarded') && $model->getGuarded() !== ['*']) {
|
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
|
private function resourceNotFoundResponse(string $resource, ?string $record = null): JsonResponse
|
||||||
{
|
{
|
||||||
$message = $record
|
$message = $record
|
||||||
@@ -305,4 +361,16 @@ class SupportResourceController extends Controller
|
|||||||
404
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,19 @@ class SupportApiRegistry
|
|||||||
return $resources[$resource] ?? null;
|
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>
|
* @return array<int, string>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Requests\Support\Resources\SupportDataExportResourceRequest;
|
||||||
|
use App\Http\Requests\Support\Resources\SupportPhotoboothSettingResourceRequest;
|
||||||
|
use App\Http\Requests\Support\Resources\SupportTenantFeedbackResourceRequest;
|
||||||
|
use App\Http\Requests\Support\Resources\SupportTenantResourceRequest;
|
||||||
|
use App\Http\Requests\Support\Resources\SupportUserResourceRequest;
|
||||||
use App\Models\BlogCategory;
|
use App\Models\BlogCategory;
|
||||||
use App\Models\BlogPost;
|
use App\Models\BlogPost;
|
||||||
use App\Models\Coupon;
|
use App\Models\Coupon;
|
||||||
@@ -57,6 +62,9 @@ return [
|
|||||||
'write' => ['support:write'],
|
'write' => ['support:write'],
|
||||||
'actions' => ['support:actions'],
|
'actions' => ['support:actions'],
|
||||||
],
|
],
|
||||||
|
'validation' => [
|
||||||
|
'update' => SupportTenantResourceRequest::class,
|
||||||
|
],
|
||||||
'mutations' => [
|
'mutations' => [
|
||||||
'create' => false,
|
'create' => false,
|
||||||
'update' => true,
|
'update' => true,
|
||||||
@@ -70,6 +78,9 @@ return [
|
|||||||
'read' => ['support:read'],
|
'read' => ['support:read'],
|
||||||
'write' => ['support:write'],
|
'write' => ['support:write'],
|
||||||
],
|
],
|
||||||
|
'validation' => [
|
||||||
|
'update' => SupportUserResourceRequest::class,
|
||||||
|
],
|
||||||
'mutations' => [
|
'mutations' => [
|
||||||
'create' => false,
|
'create' => false,
|
||||||
'update' => true,
|
'update' => true,
|
||||||
@@ -201,6 +212,9 @@ return [
|
|||||||
'read' => ['support:read'],
|
'read' => ['support:read'],
|
||||||
'write' => ['support:write'],
|
'write' => ['support:write'],
|
||||||
],
|
],
|
||||||
|
'validation' => [
|
||||||
|
'update' => SupportTenantFeedbackResourceRequest::class,
|
||||||
|
],
|
||||||
'mutations' => [
|
'mutations' => [
|
||||||
'create' => false,
|
'create' => false,
|
||||||
'update' => true,
|
'update' => true,
|
||||||
@@ -253,6 +267,9 @@ return [
|
|||||||
'read' => ['support:ops'],
|
'read' => ['support:ops'],
|
||||||
'write' => ['support:ops'],
|
'write' => ['support:ops'],
|
||||||
],
|
],
|
||||||
|
'validation' => [
|
||||||
|
'create' => SupportDataExportResourceRequest::class,
|
||||||
|
],
|
||||||
'mutations' => [
|
'mutations' => [
|
||||||
'create' => true,
|
'create' => true,
|
||||||
'update' => false,
|
'update' => false,
|
||||||
@@ -266,6 +283,9 @@ return [
|
|||||||
'read' => ['support:ops'],
|
'read' => ['support:ops'],
|
||||||
'write' => ['support:ops'],
|
'write' => ['support:ops'],
|
||||||
],
|
],
|
||||||
|
'validation' => [
|
||||||
|
'update' => SupportPhotoboothSettingResourceRequest::class,
|
||||||
|
],
|
||||||
'mutations' => [
|
'mutations' => [
|
||||||
'create' => false,
|
'create' => false,
|
||||||
'update' => true,
|
'update' => true,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Tests\Feature\Support;
|
|||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Bus;
|
||||||
use Laravel\Sanctum\Sanctum;
|
use Laravel\Sanctum\Sanctum;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@@ -34,4 +35,51 @@ class SupportApiTest extends TestCase
|
|||||||
$response->assertOk()
|
$response->assertOk()
|
||||||
->assertJsonStructure(['data', 'meta']);
|
->assertJsonStructure(['data', 'meta']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_support_resource_update_rejects_invalid_fields(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'role' => 'super_admin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
|
||||||
|
Sanctum::actingAs($user, ['support-admin', 'support:write']);
|
||||||
|
|
||||||
|
$response = $this->patchJson('/api/v1/support/tenants/'.$tenant->id, [
|
||||||
|
'data' => [
|
||||||
|
'name' => 'Unauthorized',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonPath('error.code', 'support_invalid_fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_support_data_export_create_sets_user_and_dispatches_job(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'role' => 'super_admin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
|
||||||
|
Bus::fake();
|
||||||
|
Sanctum::actingAs($user, ['support-admin', 'support:ops']);
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/v1/support/data-exports', [
|
||||||
|
'data' => [
|
||||||
|
'scope' => 'tenant',
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'include_media' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertCreated()
|
||||||
|
->assertJsonPath('data.status', 'pending')
|
||||||
|
->assertJsonPath('data.user_id', $user->id)
|
||||||
|
->assertJsonPath('data.event_id', null);
|
||||||
|
|
||||||
|
Bus::assertDispatched(\App\Jobs\GenerateDataExport::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user