402 lines
12 KiB
PHP
402 lines
12 KiB
PHP
<?php
|
|
|
|
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\Services\Audit\SuperAdminAuditLogger;
|
|
use App\Support\ApiError;
|
|
use App\Support\SupportApiAuthorizer;
|
|
use App\Support\SupportApiRegistry;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
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
|
|
{
|
|
public function index(Request $request, string $resource): JsonResponse
|
|
{
|
|
if ($response = SupportApiAuthorizer::authorizeResource($request, $resource, 'read')) {
|
|
return $response;
|
|
}
|
|
|
|
$config = SupportApiRegistry::get($resource);
|
|
if (! $config) {
|
|
return $this->resourceNotFoundResponse($resource);
|
|
}
|
|
|
|
$modelClass = $config['model'];
|
|
/** @var Builder $query */
|
|
$query = $modelClass::query();
|
|
|
|
$relations = SupportApiRegistry::withRelations($resource);
|
|
if ($relations !== []) {
|
|
$query->with($relations);
|
|
}
|
|
|
|
$this->applySearch($request, $query, $resource);
|
|
$this->applySorting($request, $query, $resource);
|
|
|
|
$perPage = $this->resolvePerPage($request);
|
|
$paginator = $query->paginate($perPage);
|
|
|
|
return response()->json([
|
|
'data' => $paginator->items(),
|
|
'meta' => [
|
|
'page' => $paginator->currentPage(),
|
|
'per_page' => $paginator->perPage(),
|
|
'total' => $paginator->total(),
|
|
'last_page' => $paginator->lastPage(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function show(Request $request, string $resource, string $record): JsonResponse
|
|
{
|
|
if ($response = SupportApiAuthorizer::authorizeResource($request, $resource, 'read')) {
|
|
return $response;
|
|
}
|
|
|
|
$model = $this->resolveRecord($resource, $record);
|
|
|
|
if (! $model) {
|
|
return $this->resourceNotFoundResponse($resource, $record);
|
|
}
|
|
|
|
return response()->json([
|
|
'data' => $model,
|
|
]);
|
|
}
|
|
|
|
public function store(SupportResourceRequest $request, string $resource): JsonResponse
|
|
{
|
|
if ($response = SupportApiAuthorizer::authorizeAnyAbility($request, SupportApiRegistry::abilitiesFor($resource, 'write'), 'write')) {
|
|
return $response;
|
|
}
|
|
|
|
if (! SupportApiRegistry::allowsMutation($resource, 'create')) {
|
|
return $this->mutationNotAllowedResponse($resource, 'create');
|
|
}
|
|
|
|
$config = SupportApiRegistry::get($resource);
|
|
if (! $config) {
|
|
return $this->resourceNotFoundResponse($resource);
|
|
}
|
|
|
|
$modelClass = $config['model'];
|
|
/** @var Model $model */
|
|
$model = new $modelClass;
|
|
|
|
$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);
|
|
|
|
app(SuperAdminAuditLogger::class)->record(
|
|
SupportApiRegistry::auditAction($resource, 'created'),
|
|
$record,
|
|
SuperAdminAuditLogger::fieldsMetadata($payload),
|
|
actor: $request->user(),
|
|
source: static::class
|
|
);
|
|
|
|
if ($resource === 'data-exports') {
|
|
GenerateDataExport::dispatch($record->id);
|
|
}
|
|
|
|
return response()->json([
|
|
'data' => $record,
|
|
], 201);
|
|
}
|
|
|
|
public function update(SupportResourceRequest $request, string $resource, string $record): JsonResponse
|
|
{
|
|
if ($response = SupportApiAuthorizer::authorizeAnyAbility($request, SupportApiRegistry::abilitiesFor($resource, 'write'), 'write')) {
|
|
return $response;
|
|
}
|
|
|
|
if (! SupportApiRegistry::allowsMutation($resource, 'update')) {
|
|
return $this->mutationNotAllowedResponse($resource, 'update');
|
|
}
|
|
|
|
$model = $this->resolveRecord($resource, $record);
|
|
|
|
if (! $model) {
|
|
return $this->resourceNotFoundResponse($resource, $record);
|
|
}
|
|
|
|
$payload = $this->validatedPayload($request, $resource, 'update', $model);
|
|
|
|
if ($payload instanceof JsonResponse) {
|
|
return $payload;
|
|
}
|
|
|
|
if ($payload === []) {
|
|
return $this->emptyPayloadResponse($resource);
|
|
}
|
|
|
|
$model->fill($payload);
|
|
$model->save();
|
|
|
|
app(SuperAdminAuditLogger::class)->record(
|
|
SupportApiRegistry::auditAction($resource, 'updated'),
|
|
$model,
|
|
SuperAdminAuditLogger::fieldsMetadata($payload),
|
|
actor: $request->user(),
|
|
source: static::class
|
|
);
|
|
|
|
return response()->json([
|
|
'data' => $model->refresh(),
|
|
]);
|
|
}
|
|
|
|
public function destroy(Request $request, string $resource, string $record): JsonResponse
|
|
{
|
|
if ($response = SupportApiAuthorizer::authorizeAnyAbility($request, SupportApiRegistry::abilitiesFor($resource, 'write'), 'write')) {
|
|
return $response;
|
|
}
|
|
|
|
if (! SupportApiRegistry::allowsMutation($resource, 'delete')) {
|
|
return $this->mutationNotAllowedResponse($resource, 'delete');
|
|
}
|
|
|
|
$model = $this->resolveRecord($resource, $record);
|
|
|
|
if (! $model) {
|
|
return $this->resourceNotFoundResponse($resource, $record);
|
|
}
|
|
|
|
$model->delete();
|
|
|
|
app(SuperAdminAuditLogger::class)->record(
|
|
SupportApiRegistry::auditAction($resource, 'deleted'),
|
|
$model,
|
|
SuperAdminAuditLogger::fieldsMetadata([]),
|
|
actor: $request->user(),
|
|
source: static::class
|
|
);
|
|
|
|
return response()->json(['ok' => true]);
|
|
}
|
|
|
|
private function resolveRecord(string $resource, string $record): ?Model
|
|
{
|
|
$config = SupportApiRegistry::get($resource);
|
|
|
|
if (! $config) {
|
|
return null;
|
|
}
|
|
|
|
$modelClass = $config['model'];
|
|
|
|
$query = $modelClass::query();
|
|
|
|
if (is_numeric($record)) {
|
|
return $query->find($record);
|
|
}
|
|
|
|
$keyName = (new $modelClass)->getKeyName();
|
|
|
|
return $query->where($keyName, $record)->first();
|
|
}
|
|
|
|
private function validatedPayload(SupportResourceRequest $request, string $resource, string $action, Model $model): array|JsonResponse
|
|
{
|
|
$payload = $request->validated('data');
|
|
|
|
if (! is_array($payload)) {
|
|
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() !== ['*']) {
|
|
$columns = Schema::getColumnListing($model->getTable());
|
|
|
|
return Arr::only($payload, $columns);
|
|
}
|
|
|
|
if ($fillable === []) {
|
|
return [];
|
|
}
|
|
|
|
return Arr::only($payload, $fillable);
|
|
}
|
|
|
|
private function applySearch(Request $request, Builder $query, string $resource): void
|
|
{
|
|
$term = $request->string('search')->trim()->value();
|
|
|
|
if ($term === '') {
|
|
return;
|
|
}
|
|
|
|
$fields = SupportApiRegistry::searchFields($resource);
|
|
|
|
if ($fields === []) {
|
|
return;
|
|
}
|
|
|
|
$columns = Schema::getColumnListing($query->getModel()->getTable());
|
|
$fields = array_values(array_intersect($fields, $columns));
|
|
|
|
if ($fields === []) {
|
|
return;
|
|
}
|
|
|
|
$query->where(function (Builder $builder) use ($fields, $term): void {
|
|
foreach ($fields as $field) {
|
|
if ($field === 'id' && is_numeric($term)) {
|
|
$builder->orWhere($field, (int) $term);
|
|
} else {
|
|
$builder->orWhere($field, 'like', "%{$term}%");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private function applySorting(Request $request, Builder $query, string $resource): void
|
|
{
|
|
$sort = $request->string('sort')->trim()->value();
|
|
|
|
if ($sort === '') {
|
|
return;
|
|
}
|
|
|
|
$direction = 'asc';
|
|
$field = $sort;
|
|
|
|
if (str_starts_with($sort, '-')) {
|
|
$direction = 'desc';
|
|
$field = ltrim($sort, '-');
|
|
}
|
|
|
|
$allowed = SupportApiRegistry::searchFields($resource);
|
|
$allowed[] = 'id';
|
|
|
|
$columns = Schema::getColumnListing($query->getModel()->getTable());
|
|
$allowed = array_values(array_intersect($allowed, $columns));
|
|
|
|
if (! in_array($field, $allowed, true)) {
|
|
return;
|
|
}
|
|
|
|
$query->orderBy($field, $direction);
|
|
}
|
|
|
|
private function resolvePerPage(Request $request): int
|
|
{
|
|
$default = (int) config('support-api.pagination.default_per_page', 50);
|
|
$max = (int) config('support-api.pagination.max_per_page', 200);
|
|
|
|
$perPage = (int) $request->input('per_page', $default);
|
|
|
|
if ($perPage < 1) {
|
|
$perPage = $default;
|
|
}
|
|
|
|
return min($perPage, $max);
|
|
}
|
|
|
|
private function mutationNotAllowedResponse(string $resource, string $action): JsonResponse
|
|
{
|
|
return ApiError::response(
|
|
'support_mutation_not_allowed',
|
|
'Mutation Not Allowed',
|
|
"{$resource} does not allow {$action} operations in support API.",
|
|
403
|
|
);
|
|
}
|
|
|
|
private function emptyPayloadResponse(string $resource): JsonResponse
|
|
{
|
|
return ApiError::response(
|
|
'support_invalid_payload',
|
|
'Invalid Payload',
|
|
"No mutable fields provided for {$resource}.",
|
|
422
|
|
);
|
|
}
|
|
|
|
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
|
|
? "{$resource} record not found."
|
|
: "Support resource {$resource} is not registered.";
|
|
|
|
return ApiError::response(
|
|
'support_resource_not_found',
|
|
'Not Found',
|
|
$message,
|
|
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;
|
|
}
|
|
}
|