Laravel Nova - How to hide 'Create' button from HasMany field? - php

I've User model which has HasMany relation with Post model. When I include a field for HasMany in User resource of Nova, I see there is Create post button. How do I remove/hide that button?

You could achieve this with Policies.
According to the documentation:
If a policy exists but is missing a method for a particular action, the user will not be allowed to perform that action. So, if you have defined a policy, don't forget to define all of its relevant authorization methods.
So in your case, if you want to hide the button completely, just create a policy for your resource (PostPolicy) and don't implement the create method.

You need to 2 things here.
In your Post resource
public static function authorizable()
{
return true;
}
Now create policy for Post and return true for all methods except create, for create return false and in AuthServiceProvider.php
put
protected $policies = [
Post::class => PostPolicy::class,
];
And you are done.

this question is answered in laravel nova official documentation
in my case i have user model and order model, user Hasmany order i added
public function addOrder()
{
return false;
}
on user policy now create role button is gone on user detail page
this is a screenshot of user detail page

If you're like me, the last thing you want to do is set a policy blocking creation of the sub-resource referenced by the HasMany rule by setting a policy. The reason is that setting this addX() policy to false on the "Has" side of the HasMany not only blocks the creation of the sub-resource from the resource detail view, it also produces permission errors when creating the sub-resource from its page view, specifically that creation of the resource with references to the "parent" or "Has" is forbidden by the policy. Which when you think about how broad the permission statement of addClassName() is, isn't actually surprising.
Thus my solution ended up having to be butt ugly CSS. Just why is this the only way to do page dependant hiding of the create button. This should be a HasMany::make("")->canCreate(false) declaration in the Nova/*.php view file.
Anyway here's the CSS, hopefully, it helps someone.
div[dusk="parent-class-detail-component"] div[dusk="has-many-child-class-index-component"] a[dusk='create-button'] {
display: none;
}

In case someone is still looking for the solution, you can authorise attaching/detaching resources in your policies:
https://nova.laravel.com/docs/2.0/resources/authorization.html#authorizing-attaching-detaching
So in this case, you have a UserPolicy to which you add a function:
attachPost(User $user, User $model, Post $post)
{
return false;
}
The $user variable is the user that is signed in, the $model variable is the user page that is viewed.

Related

How to prevent users from checking not existing urls, how to hide routes in Laravel 8.*

First question was solved with findOrFail method
Is there any way to prevent users from checking non-existing routes?
Example
I've got route to http://127.0.0.1:8000/event/9
but event with id 8 does not exist, if user would go to that id there is a massage:
Attempt to read property "photo_patch" on null (View: C:\xampp\htdocs\Laravel1\resources\views\frontend\eventView.blade.php)
Or any other error from db that record does not exist.
Second question
How to turn on preety URLs in laravel
So my page with display http://127.0.0.1:8000 not http://127.0.0.1:8000/events something...
I know that its somewere in config files but I cant find it.
Example class and route that uses it:
-----------------------------Class----------------
public function eventView($id)
{
$notDisplay = Auth::user();
$eventView = Event::findOrFail($id);
if(!$notDisplay){
$eventView->displayed = $eventView->displayed +1;
$eventView->save();
}
return view('frontend/eventView', ['eventView' => $eventView]);
}
----------------Route-----------------
Route::get('event/' . '{id}', [App\Http\Controllers\FrontendController::class, 'eventView'])->name('eventView');
First off, use the container!
Laravel's service container is very powerful and your controller resolve use-case is one of the most common places you should be using it. The url argument and controller argument MUST match for this to work.
Your route:
Route::get('event/' . '{event}', [App\Http\Controllers\FrontendController::class, 'eventView'])->name('eventView');
Your Controller:
public function eventView(Event $event)
{
return view('frontend/eventView', ['event' => $event]);
}
When leveraging Laravel's dependency injection and container, you get your findOrFail() for free. You should also remove your auth check, and handle that with route middleware.
In terms of "prettifying" urls, Laravel's route model binding feature allows you to control what property of a model is used to for container resolution. For example, let's imagine your event has a unique slug you'd like to use instead of the auto-increment id:
Route::get('event/' . '{event:slug}', [App\Http\Controllers\FrontendController::class, 'eventView'])->name('eventView');
Laravel's routing functionality offers a fallback feature that would allow you to fine-tune where the user is redirected if the route model binding failed.
https://laravel.com/docs/8.x/routing#fallback-routes
With regard to preventing an unauthorized individual from editing someone else's event. The first place I would put protections in place would be at the time of persistence (when saving to the database). While you can do this in every place in your codebase where persistence occurs, Laravel's Observer feature could be a great fit. That way, you can be confident that no matter what code is added to your app, the ownership check will always be run before making any changes to events.
https://laravel.com/docs/8.x/eloquent#observers
The second place that I would put protections in place would be with a route middleware on any routes that can mutate the event. That way, you can redirect the user away from an event they don't own before they even have a chance to attempt to edit it.
https://laravel.com/docs/8.x/middleware#assigning-middleware-to-routes

Laravel policy autodetect

today i was creating USER profile page with is controlled in ProfileController it returning views to profile page, profile settings, etc.
so i decide to make some Policy rules to Edit profile and etc.
so i found i should use Middleware / Gates / Policy, based on Laravel Doc i chose Policy because profil page is public but only specific part of it can author edit so i needed #can
So my steps:
php artisan make:policy ProfilePolicy ( without model )
Registered policy to AuthServiceProvider in $policies property
writed methods like edit inside ProfilePolicy
then i started thinking how i define it to my Controller hmmm, documentation doesnt helps me :/
so i tryed blade #can('edit', $user) method and it worked, but HOW ?, how to define specific policy to one Controller ? ( not Model ), how to define multiple Policy to single Controller
i m lost how laravel Magic done this maybe because of Naming ? ProfileController => ProfilePolicy ?
In the controller you can write this
public function edit(Profile $profile) {
$this->authorize('edit', $profile)
}
Laravel does this:
Check the type of $profile, and it's a Profile::class
Check policies registered for that class (your step 2)
Looks for the edit method in that policy, if not found, return false meaning user is not authorized
Executes the edit() function that returns true/false
In blade the #can directive does exactly the same thing.
Policies are meant to be tied to Models, it's a convenient way to write rules to handle single models, but they can be triggered in many ways (like the authorize() method in controllers and #can directive in blade).

Check user access with middleware string

I have a String with a middleware rule (like in routes):
$middleware = "can:index,App\Models\Order";
Is there any possiblity to check if a given user has access with this middleware rule?
That is how laravel policies define authorization rules in middleware. See here: https://laravel.com/docs/5.5/authorization#via-middleware
Create a policy class for the model you are authorizing then register the policy to model.
It is possible to see if a given user is authorized to carry out a certain action in many different ways. One is by using the middleware in your question and attaching it to a route or group. Here are some other ways:
Using the can method on the User object. The can method is inherited from the Authorizable trait (So it's not limited just to users):
if ($user->can('index', 'App\Models\Order')) {
// User is allowed to index orders.
}
Using the authorize method on a controller. The authorize method is inherited from the AuthorizesRequests trait (So this is not limited to just controller). This method throws an exception if authorization fails:
$this->authorize('index', 'App\Models\Order');
In a view, it is possible to use the #can Blade directive to see if a user is authorized to carry out the given action:
#can('index', 'App\Models\Order')
This user can index orders.
#endcan
If you have that specific string, you could do a little bit of manipulation to extract the bits you need and then pass it to one of the above methods:
$middleware = "can:index,App\Models\Order";
list($rule, $parameters) = explode(':', $middleware);
list($ability, $model) = explode(',', $parameters);
if ($user->can($ability, $model)) {
// User can index orders.
}
Of course it would be wise to do more error checking etc.

Laravel: simplest/right way to save a new step after login?

I'm having my first interaction with the core laravel code so I want to be careful not to break anything.
In my project, my users also correspond to person records (via user->person_id), so I have a get_person_from_user() function that takes the \Auth::user() (conveniently accessible anywhere) and returns the person object, so I can grab the person record for the authenticated user from any controller and pass it to a view.
The problem: there's a piece of data from the person record that I'd like to include in a nav partial in my default blade view (which gets extended by a bunch of different views), so it's the one case where I'm not going through a controller first. I'm unclear on how I can make the logged in user's person record available here. Any suggestions?
I think I need to add some step after the user logs in, to save the person record (globally? in the session?) so it's generally accessible. The login stuff happens in AuthenticatesUsers.php, and reading around it sounds like I'll want to add an override of postLogin to my AuthController.
But I tried copying that function from AuthenticatesUsers.php into my AuthController (not adding anything else to it yet), and AuthController gives me a new error when I try to log in:
ReflectionException in RouteDependencyResolverTrait.php line 81:
Class App\Http\Controllers\Auth\Request does not exist
Any advice on a good way to go about accessing the person object for the authenticated user, when I don't have a controller to pass it along?
You can setup the correct relationship on the User model to Person model.
public function person()
{
return $this->belongsTo(Person::class);
}
Then you can do:
Auth::user()->person;
For having a variable available to a particular view you can use a View Composer. (You can create and register a Service Provider and add this to the register method.) Potentially something like this:
view()->composer('someview', function ($view) {
if ($user = Auth::user()) {
$somevar = $user->person->somevar;
} else {
$somevar = null; // or some default
}
$view->with('somevar', $somevar);
});
If this view is also used in a scenario where someone doesn't have to be authed you will want to check if Auth::user() is null before trying to use the relationship.
Laravel Docs - Views - View Composers
I suggest you to use Eloquent relation
User.php
public function person()
{
return $this->belongsTo('NAMESPACE_TO_YOUR_MODEL\Person'); //also you can specify FK, more info in docs
}
then you can access Auth facade in your view
Auth::user()->person

Laravel - Implicit route model binding with soft deleted data

I am having a small issue. There are two user roles and one is a normal member and one is an admin. The member can delete a blog and they will not be able to see the blog after they delete (soft delete) it while the admin can still see the blog, even if it's soft deleted.
Example code:
// Route file
Route::get('/blog/{blog}', 'BlogController#show');
// BlogController
public function show(App\Blog $blog) {
// It never gets to here if the blog has been soft deleted...
// Automatically throws an 404 exception
}
I want the admin to be able to visit the blog even if it's soft deleted but it doesn't really work. I am trying to edit the route service provider but I haven't gotten any luck as it doesn't let me use the Auth::user() function to get the logged in user so I can check if they have permission.
My RouteServiceProvider
$router->bind('post', function($post) {
if (Auth::user()->isAdmin()
return Post::withTrashed()->where('id', $post)->firstOrFail();
});
This does not work as it doesn't know what Auth::user() is. I have imported Auth facade but still doesn't work.
Edit: It gives me a null value when I dump and die Auth::user().
Any help is highly appreciated.
As this question pop ups when googling for this, in newer Laravel versions you can do this:
Route::get('posts/{post}', [PostController::class, 'show'])->withTrashed();
See: Implicit Soft Deleted Models
I just found out that getting the current logged in user is not possible in Route Service Provider because it loads before all session service provider.
Instead I simply did:
//Route Service Provider
$router->bind('post', function($post)
return Post::withTrashed()->where('id', $post)->firstOrFail();
});
// Controller
public function show(Post $post) {
// If the post has been trashed and the user is not admin, he cannot see it
if (!Auth::user()->isAdmin() && $post->trashed())
abort(404);
// Proceed with normal request because the post has not been deleted.
}

Categories