diff --git a/src/Fields/Field.php b/src/Fields/Field.php index e5c9f591d..949b0a613 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -226,7 +226,7 @@ public function fillAttribute(RestifyRequest $request, $model, int $bulkRow = nu } if ($request->isStoreRequest() && is_callable($this->storeCallback)) { - return $model->{$this->attribute} = call_user_func( + return call_user_func( $this->storeCallback, $request, $model, $this->attribute, $bulkRow ); } @@ -238,7 +238,7 @@ public function fillAttribute(RestifyRequest $request, $model, int $bulkRow = nu } if ($request->isUpdateRequest() && is_callable($this->updateCallback)) { - return $model->{$this->attribute} = call_user_func( + return call_user_func( $this->updateCallback, $request, $model, $this->attribute, $bulkRow ); } diff --git a/src/Http/Controllers/RepositoryAttachController.php b/src/Http/Controllers/RepositoryAttachController.php index c25a5eefa..11299790e 100644 --- a/src/Http/Controllers/RepositoryAttachController.php +++ b/src/Http/Controllers/RepositoryAttachController.php @@ -14,7 +14,7 @@ class RepositoryAttachController extends RepositoryController public function __invoke(RepositoryAttachRequest $request) { $model = $request->findModelOrFail(); - $repository = $request->repository()->allowToUpdate($request); + $repository = $request->repository(); if (is_callable($method = $this->guessMethodName($request, $repository))) { return call_user_func($method, $request, $repository, $model); @@ -23,6 +23,7 @@ public function __invoke(RepositoryAttachRequest $request) return $repository->attach( $request, $request->repositoryId, collect(Arr::wrap($request->input($request->relatedRepository))) + ->filter(fn ($relatedRepositoryId) => $request->repository()->allowToAttach($request, $request->attachRelatedModels())) ->map(fn ($relatedRepositoryId) => $this->initializePivot( $request, $model->{$request->viaRelationship ?? $request->relatedRepository}(), $relatedRepositoryId )) diff --git a/src/Http/Controllers/RepositoryDetachController.php b/src/Http/Controllers/RepositoryDetachController.php index 1adeb4a1d..0feccd109 100644 --- a/src/Http/Controllers/RepositoryDetachController.php +++ b/src/Http/Controllers/RepositoryDetachController.php @@ -17,7 +17,8 @@ public function __invoke(RepositoryDetachRequest $request) return $repository->detach( $request, $request->repositoryId, collect(Arr::wrap($request->input($request->relatedRepository))) - ->map(fn ($relatedRepositoryId) => $this->initializePivot( + ->filter(fn ($relatedRepositoryId) => $request->repository()->allowToDetach($request, $request->detachRelatedModels())) + ->map(fn ($relatedRepositoryId) => $this->initializePivot( $request, $model->{$request->viaRelationship ?? $request->relatedRepository}(), $relatedRepositoryId )) ); diff --git a/src/Http/Requests/RepositoryAttachRequest.php b/src/Http/Requests/RepositoryAttachRequest.php index 972af0822..6490f8775 100644 --- a/src/Http/Requests/RepositoryAttachRequest.php +++ b/src/Http/Requests/RepositoryAttachRequest.php @@ -2,6 +2,23 @@ namespace Binaryk\LaravelRestify\Http\Requests; +use Binaryk\LaravelRestify\Restify; +use Illuminate\Support\Arr; +use Illuminate\Support\Collection; + class RepositoryAttachRequest extends RestifyRequest { + public function attachRelatedModels(): Collection + { + $relatedRepository = $this->repository( + Restify::repositoryForTable($table = $this->relatedRepository)::uriKey() + ); + + if (is_null($relatedRepository)) { + abort(400, "Missing repository for the [$table] table"); + } + + return collect(Arr::wrap($this->input($this->relatedRepository))) + ->map(fn ($id) => $relatedRepository->model()->newModelQuery()->whereKey($id)->first()); + } } diff --git a/src/Http/Requests/RepositoryDetachRequest.php b/src/Http/Requests/RepositoryDetachRequest.php index 15987806d..a7e29d1e5 100644 --- a/src/Http/Requests/RepositoryDetachRequest.php +++ b/src/Http/Requests/RepositoryDetachRequest.php @@ -2,6 +2,23 @@ namespace Binaryk\LaravelRestify\Http\Requests; +use Binaryk\LaravelRestify\Restify; +use Illuminate\Support\Arr; +use Illuminate\Support\Collection; + class RepositoryDetachRequest extends RestifyRequest { + public function detachRelatedModels(): Collection + { + $relatedRepository = $this->repository( + Restify::repositoryForTable($table = $this->relatedRepository)::uriKey() + ); + + if (is_null($relatedRepository)) { + abort(400, "Missing repository for the [$table] table"); + } + + return collect(Arr::wrap($this->input($this->relatedRepository))) + ->map(fn ($id) => $relatedRepository->model()->newModelQuery()->whereKey($id)->first()); + } } diff --git a/src/Repositories/Repository.php b/src/Repositories/Repository.php index 83b4cee33..2a3ae52c5 100644 --- a/src/Repositories/Repository.php +++ b/src/Repositories/Repository.php @@ -690,6 +690,8 @@ public function update(RestifyRequest $request, $repositoryId) return $this->resource; }); + $this->updateFields($request)->each(fn (Field $field) => $field->invokeAfter($request, $this->resource)); + return $this->response() ->data($this->serializeForShow($request)) ->success(); @@ -754,6 +756,24 @@ public function allowToUpdate(RestifyRequest $request, $payload = null): self return $this; } + public function allowToAttach(RestifyRequest $request, Collection $attachers): self + { + $methodGuesser = 'attach'.Str::studly($request->relatedRepository); + + $attachers->each(fn ($model) => $this->authorizeToAttach($request, $methodGuesser, $model)); + + return $this; + } + + public function allowToDetach(RestifyRequest $request, Collection $attachers): self + { + $methodGuesser = 'detach'.Str::studly($request->relatedRepository); + + $attachers->each(fn ($model) => $this->authorizeToDetach($request, $methodGuesser, $model)); + + return $this; + } + public function allowToUpdateBulk(RestifyRequest $request, $payload = null): self { $this->authorizeToUpdateBulk($request); diff --git a/src/Repositories/ValidatingTrait.php b/src/Repositories/ValidatingTrait.php index 9dfb18eeb..64d193050 100644 --- a/src/Repositories/ValidatingTrait.php +++ b/src/Repositories/ValidatingTrait.php @@ -98,6 +98,26 @@ public static function validatorForUpdate(RestifyRequest $request, $resource = n }); } + public static function validatorForAttach(RestifyRequest $request, $resource = null, array $plainPayload = null) + { + /** * @var Repository $on */ + $on = $resource ?? static::resolveWith(static::newModel()); + + $messages = $on->collectFields($request)->flatMap(function ($k) { + $messages = []; + foreach ($k->messages as $ruleFor => $message) { + $messages[$k->attribute.'.'.$ruleFor] = $message; + } + + return $messages; + })->toArray(); + + return Validator::make($plainPayload ?? $request->all(), $on->getUpdatingRules($request), $messages)->after(function ($validator) use ($request) { + static::afterValidation($request, $validator); + static::afterUpdatingValidation($request, $validator); + }); + } + public static function validatorForUpdateBulk(RestifyRequest $request, $resource = null, array $plainPayload = null) { /** * @var Repository $on */ diff --git a/src/Restify.php b/src/Restify.php index 384085ac9..9eeae4469 100644 --- a/src/Restify.php +++ b/src/Restify.php @@ -52,7 +52,7 @@ public static function repositoryForKey($key) } /** - * Get the repository class name for a given key. + * Get the repository class name for a given model. * * @param string $model * @return string @@ -68,6 +68,19 @@ public static function repositoryForModel($model) }); } + /** + * Get the repository class name for a given table name. + * + * @param string $table + * @return string + */ + public static function repositoryForTable($table) + { + return collect(static::$repositories)->first(function ($value) use ($table) { + return app($value::$model)->getTable() === $table; + }); + } + /** * Register the given repositories. * diff --git a/src/Traits/AuthorizableModels.php b/src/Traits/AuthorizableModels.php index bbd9acf2b..89e948fc1 100644 --- a/src/Traits/AuthorizableModels.php +++ b/src/Traits/AuthorizableModels.php @@ -142,6 +142,36 @@ public function authorizeToUpdate(Request $request) $this->authorizeTo($request, 'update'); } + public function authorizeToAttach(Request $request, $method, $model) + { + if (! static::authorizable()) { + return true; + } + + $authorized = method_exists(Gate::getPolicyFor($this->model()), $method) + ? Gate::check($method, [$this->model(), $model]) + : true; + + if (false === $authorized) { + throw new AuthorizationException(); + } + } + + public function authorizeToDetach(Request $request, $method, $model) + { + if (! static::authorizable()) { + return true; + } + + $authorized = method_exists(Gate::getPolicyFor($this->model()), $method) + ? Gate::check($method, [$this->model(), $model]) + : true; + + if (false === $authorized) { + throw new AuthorizationException(); + } + } + public function authorizeToUpdateBulk(Request $request) { $this->authorizeTo($request, 'updateBulk'); diff --git a/tests/Controllers/RepositoryAttachControllerTest.php b/tests/Controllers/RepositoryAttachControllerTest.php index 24e2ade68..07a1eadf3 100644 --- a/tests/Controllers/RepositoryAttachControllerTest.php +++ b/tests/Controllers/RepositoryAttachControllerTest.php @@ -3,7 +3,10 @@ namespace Binaryk\LaravelRestify\Tests\Controllers; use Binaryk\LaravelRestify\Tests\Fixtures\Company\Company; +use Binaryk\LaravelRestify\Tests\Fixtures\Company\CompanyPolicy; +use Binaryk\LaravelRestify\Tests\Fixtures\User\User; use Binaryk\LaravelRestify\Tests\IntegrationTest; +use Illuminate\Support\Facades\Gate; class RepositoryAttachControllerTest extends IntegrationTest { @@ -63,4 +66,64 @@ public function test_after_attach_a_user_to_company_number_of_users_increased() $usersFromCompany = $this->getJson('/restify-api/users?viaRepository=companies&viaRepositoryId=1&viaRelationship=users'); $this->assertCount(1, $usersFromCompany->json('data')); } + + public function test_policy_to_attach_a_user_to_a_company() + { + Gate::policy(Company::class, CompanyPolicy::class); + + $user = $this->mockUsers(2)->first(); + $company = factory(Company::class)->create(); + $this->authenticate( + factory(User::class)->create() + ); + + $_SERVER['allow_attach_users'] = false; + + $this->postJson('restify-api/companies/'.$company->id.'/attach/users', [ + 'users' => $user->id, + 'is_admin' => true, + ]) + ->assertForbidden(); + + $_SERVER['allow_attach_users'] = true; + + $this->postJson('restify-api/companies/'.$company->id.'/attach/users', [ + 'users' => $user->id, + 'is_admin' => true, + ]) + ->assertCreated(); + } + + public function test_policy_to_detach_a_user_to_a_company() + { + Gate::policy(Company::class, CompanyPolicy::class); + + $user = $this->mockUsers(2)->first(); + $company = factory(Company::class)->create(); + $this->authenticate( + factory(User::class)->create() + ); + + $this->postJson('restify-api/companies/'.$company->id.'/attach/users', [ + 'users' => $user->id, + 'is_admin' => true, + ]) + ->assertCreated(); + + $_SERVER['allow_detach_users'] = false; + + $this->postJson('restify-api/companies/'.$company->id.'/detach/users', [ + 'users' => $user->id, + 'is_admin' => true, + ]) + ->assertForbidden(); + + $_SERVER['allow_detach_users'] = true; + + $this->postJson('restify-api/companies/'.$company->id.'/detach/users', [ + 'users' => $user->id, + 'is_admin' => true, + ]) + ->assertNoContent(); + } } diff --git a/tests/Fixtures/Company/CompanyPolicy.php b/tests/Fixtures/Company/CompanyPolicy.php new file mode 100644 index 000000000..0b1b6a0f4 --- /dev/null +++ b/tests/Fixtures/Company/CompanyPolicy.php @@ -0,0 +1,126 @@ +