Allow CRUD only to Auth::user that owns the ressource - laravel

I currently work on a project where the User creates Models, that only he/she is allowed to see, edit or delete.
The Create Part done by Eloquent Relationships, but for the other operations I would like to combine it with Route Model binding and not manually in the controller. I tried solving it with middlewares, but I couldn't access the Ressource.
Can somebody point me to the right Direction, any best Practices are welcome!

Personally I use route model binding, but only allow the model to bind if the user owns the record.
This means that no matter what - people can never access someone elses record. So for example, in my route I can do
$router->get('property/{property}, ['uses' => PropertyController#show]);
Then in my RouteServiceProvider:
$router->bind('property', function($value) {
$property = \App\Property::findOrFail($value);
if ((int)$property->user_id !== (int)auth()->id()) {
abort (404);
}
return $property;
});
So in the example above - we have a property route, and it will try and find the property record given. It will then check that the user owns the record, otherwise it throws a 404 (but you could just redirect or something - up to you).

Related

Laravel create related Model view, pass prefilled value

i am implementing a backoffice system with many Models and Relations.
Now im stuck with my UI Stragegy:
Lets assume i have Houses and Rooms. One House has many Rooms.
I have created controllers for both Models the "Laravel" way.(Resource Controllers)
So i have routes for both of them
example.com/backoffice/house
example.com/backoffice/room
What i want to implement sounds simple:
I want an Button inside the Detail View of a House ("Create Room for this House") which redirects me to "room/create" but in the create view i want to set the value for "house_id" to the id of the House i am comming from. So i can normaly use the store method in the RoomController and then redirect back to the house.
I want a general way because i must use this function on many Models/Views. I am thinking about a session variable but i think eventually has someone a better way for generally handling such cases? Or a better idea for UI Handling?
Apparently, Laravel removed some of their awesome documentation for version 5.6, being nested resource controllers.
What you could do, is use nested routes.
Let's assume your current controllers are set up the following way:
Route::resource('houses', 'HouseController');
Route::resource('rooms', 'RoomController');
If you change this part to the following:
Route::resource('houses', 'HouseController');
Route::resource('houses.rooms', 'RoomController');
This couples every room to a house and is really easy to manage. It gives you URL's like houses/4/rooms/create, which gives you a house_id in your create method instantly:
public function create($houseId)
{
return view('houses.rooms.create', ['houseId' => $houseId]);
}
If you want to edit a room, it is exactly the same:
public function edit($houseId, $roomId)
The Laravel 5.1 documentation still has an example of this technique.
To do this, i would suggest the following way (there might be other ways also)
Change Route:
Route::get('room/create/{house_id?},'Controller#action')->name('room.create')
Add <a> tag in house_view.blade.php file.
Create Room for this House
Room Controller file.
public function formCreate($house_id)
{
return view('form.room_create', ['house_id' => $house_id]);
}
Add type hidden <input> tag in room_create.blade.php file
<input type="hidden" id="house_id" name="house_id" value="{{$house_id or ''}}">

Search object by slug and not by id

I'm a relative beginner with Laravel (using version 5.2.3) and have been working through tutorials on Laracasts and then doing a bit of my own experimenting.
I successfully set up a route that fetches an item from a table by its ID, as shown below
Route::get('/wiseweasel/{id}', 'WiseweaselController#singleArticle');
For simplicity, the controller simply dd's the article
public function singleArticle($id)
{
$article = ww_articles::find($id);
dd($article);
}
This works absolutely fine - I visit eg /wiseweasel/2 and get the contents of the record with id2.
So, I then wanted to use the slug field from the record instead of the id. Since I know the ID method was working, I've tried just modifying this route and controller (also tried creating anew, neither worked) So I now have:
Route::get('/wiseweasel/{slug}', 'WiseweaselController#singleArticle');
and
public function singleArticle($slug)
{
$article = ww_articles::find($slug);
dd($article);
}
The slug for the second record is "secondarticle". So, visiting the url /wiseweasel/secondarticle, I would expect to see the same record as previously dd'd out. Instead, I end up with null.
Even more oddly, using the original id route (/wiseweasel/2) still returns the record... when I have removed all trace of this from the routes and controller, so I would expect this to fail...
This is making me wonder if this could be some odd caching issue? I've tried
php artisan route:clear
in case the route was being cached. I've also tried restarting both Apache and MySql (I'm using XAMMP for both).
Still no luck though... not sure if I've misunderstood how something works or what's going on... so if anyone has any suggestions as to what I might have done wrong, or anything to try, I would be very grateful! :)
You also have the option of using Route Model Binding to take care of this and inject the resolved instance into your methods.
With the new implicit Route Model Binding you can tell the model what key it should use for route binding.
// routes
Route::get('/wiseweasel/{article}', 'WiseweaselController#singleArticle');
// Article model
public function getRouteKeyName()
{
return 'slug';
}
// controller
public function singleArticle(Article $article)
{
dd($article);
}
Laravel Docs - Route Model Binding
Laravel won't automatically know that for slug it should search record in different way.
When you are using:
$article = ww_articles::find($slug);
you are telling Laravel - find record of www_articles by ID. (no matter you call this id $slug).
To achieve what you want change:
$article = ww_articles::find($slug);
into
$article = ww_articles::where('slug', $slug)->first();
This will do the trick (for slug put the name of column in table in database). Of course remember that in this case slug should be unique in all records or you won't be able to get all the slugs.
Maybe it's a bit late for the answer but there is another way to keep using find method and use slug as your table identifier. You have to set the protected $primaryKey property to 'slug' in your model.
class ww_articles extends Model
{
protected $primaryKey = 'slug';
...
}
This will work because find method internally uses the getQualifiedKeyName method from Model class which uses the $primaryKey property.
If you have both routes like this
Route::get('/wiseweasel/{id}', 'WiseweaselController#singleArticle');
Route::get('/wiseweasel/{slug}', 'WiseweaselController#singleArticle');
it will always use the first one. Obviously, there is no id 'secondarticle', so it returns null (although in this case it doesn't matter, they both point to the same method).
The reason is route will search through possible routes till it finds a matching, which is always the one with {id}. Why? You're not telling Route that {id} must match an integer!
You can make sure {id} is understood as an integer, however I suggest using urls like this is a better option
/wiseweasel/{id}/{slug?}
Another suggestion. Do not use names such as xx_articles for a model, but Article instead. This way you can use the new implicit route binding. So using implicit route binding your url would look like this (assuming your model is called Article)
Route::get('/wiseweasel/{article}', 'WiseweaselController#singleArticle');

Sentry & Laravel, getting users within a group. changing findAllUsersWithAccess to have pagination

I'm trying to find all users w/ a specific permissions list in Sentry with laravel. The problem is that Sentry::findAllUsersWithAccess() returns an array().
as stated in their github repository i pinpointed their code to be
public function findAllWithAccess($permissions)
{
return array_filter($this->findAll(), function($user) use ($permissions)
{
return $user->hasAccess($permissions);
});
}
right now, it gets all users and filter it out with users with permission list. the big problem would be when I as a developer would get the set of users, it'll show ALL users, i'm developing an app which may hold thousands of users and i only need to get users with sepcific permission lists.
With regards to that would love to use one with a ->paginate() capability.
Any thoughts how to get it without getting all the users.
Why dont you override the findAllWithAccess() method and write your own implementation, which uses mysql where instead of array_filter().
I dont know your project structure and the underlying db schema, so all i can give you atm is the link to the eloquent documentation Querying Relations (whereHas).
In case you dont know where to start: its always a good idea to look at the ServiceProvider (SentryServiceProvider, where the UserProvider, which holds the findAllWidthAccess() method, is registered). Override the registerUserProvider method and return your own implementation of the UserProvider (with the edited findAllWithAccess() method).
Hope that will point you in the right direction.
In Laravel you can do pagination manually on arrays:
$paginator = Paginator::make($items, $totalItems, $perPage);
Check the docs: http://laravel.com/docs/pagination

MVC Putting an action in the most appropriate correct controller

I was just wondering what the best practice approach is for deciding where to create an action/view in certain situations.
If User hasMany Video
where is the best place to create the action/view to show user videos?
So within the Users account page 'My Videos' link do you
just create a users/my_videos action and view.
create videos/my_videos action and view.
or as is most likely you would already have a Controller/Action of videos/index which would have search functionality. Simply use this passing in a user id.
Any thoughts/advice greatly appreciated
Thanks
Leo
One potential option is to do the following:
Since the videos likely have much more code around them than a simple which user has which videos lookup the video list action should be in the VideosController.
In past projects I have (in CakePHP 1.3) used prefix routing to address some of this.
In config/core.php make sure you enable routing.prefixes to include a 'user' prefix.
<?php
... in routes.php ...
Routing.prefixes = array( 'user' );
?>
In the videos controller make an action with the following signature:
<?php
...
public function user_index( $userID = null ){
...
}
?>
and in the views where you link to the list of users videos the html::link call should look similar to the following:
<?php
...
echo $this->Html->link( 'User\'s Videos', array(
'controller' => 'videos',
'action' => 'index',
'prefix' => 'user',
$this->Session->read( 'Auth.User.id' )
));
?>
Of course this assumes you are using the Auth component here to track the logged in user. The Session helper code to read the authenticated user id might need tweaking.
This lets you a) Not worry too much about routing aside from enabling prefix routing and b) will quickly let you have pretty links like so -- site.com/user/videos/index/419
Couple this with some Slug love ( this is the best link for this I have seen - no slug field required on the db layer - http://42pixels.com/blog/slugs-ugly-bugs-pretty-urls )
You could even end up with urls like so quite easily: site.com/user/videos/index/eben-roux
and with just a tiny bit of editing to app/config/routes.php you could eliminate the /index/ portion and the results would be SEO friendly and user friendly in the format:
site.com/user/videos/eben-roux
http://book.cakephp.org/view/945/Routes-Configuration
As always with code you have the two extremes of:
1) Putting everything in a single controller
2) Having every action in a separate controller
The ideal approach will nearly always be somewhere between the two so how to decide what is grouped together and what is separated?
In MVC I tend to look at the Views and see what the commonalities are: as you point out Users have a ref to a collection of Videos in the Model, but would you want both sets of Data in any single View? i.e. In this example is it likely that you would be on a page that both managed user details, and displayed the list of vids? If not then I'd suggest separate controllers.
If either controller would then be extremely simple - e.g. one method, then may be worth considering merging the two.
I like to keeps things separate.
What I'd do is an index action in videos controller, passing user's id as argument and then displaying only current users video.
public function index($id = null){
$this->paginate = array( 'conditions'=> array('Video.user_id' => $id));
$this->set('videos', $this->paginate());
}
My take is that it depends on the responsibility you assign to the controllers.
I would say that something like a User or a Video controller should be concerned with only those entities.
You may want to consider something like a UserDashboard (or something similar but appropriately named) as alluded to by Dunhamzzz in the comments. This can aggegate all the functionality from an "entry" point-of-view. The same way a banner / shortcut / action menu would work.
Your UserDashboard would use whatever data layer / repository is required to get the relevant data (such as the IVideoRepository or IVideoQuery implementation).
Usually when something doesn't feel right it isn't. Try splitting it out and see how it works. You can alsways re-arrange / refactor again later.
Just a thought.
I don't think there's a 'one-rule-fits-all' solution to this question, but I would try to take an approach in which you would determine what the main object is that you're dealing with, and adding the action/view to that object's controller.
In your example I'd say that your main object is a video and that the action you're requiring is a list of video's filtered by a specific property (in this case the user's id, but this could very well be a category, a location, etc.).
One thing I would not do is let your desired URL determine in which controller you put your functionality. URLs are trivially changed with routes.

CakePHP, organize site structure around groups

So, I'm not quite sure how I should structure this in CakePHP to work correctly in the proper MVC form.
Let's, for argument sake, say I have the following data structure which are related in various ways:
Team
Task
Equipment
This is generally how sites are and is quite easy to structure and make in Cake. For example, I would have the a model, controller and view for each item set.
My problem (and I'm sure countless others have had it and already solved it) is that I have a level above the item sets. So, for example:
Department
Team
Task
Equipment
Department
Team
Task
Equipment
Department
Team
Task
Equipment
In my site, I need the ability for someone to view the site at an individual group level as well as move to view it all together (ie, ignore the groups).
So, I have models, views and controls for Depart, Team, Task and Equipment.
How do I structure my site so that from the Department view, someone can select a Department then move around the site to the different views for Team/Task/Equipment showing only those that belong to that particular Department.
In this same format, is there a way to also move around ignoring the department associations?
Hopefully the following example URLs clarifies anything that was unclear:
// View items while disregarding which group-set record they belong to
http://www.example.com/Team/action/id
http://www.example.com/Task/action/id
http://www.example.com/Equipment/action/id
http://www.example.com/Departments
// View items as if only those associated with the selected group-set record exist
http://www.example.com/Department/HR/Team/action/id
http://www.example.com/Department/HR/Task/action/id
http://www.example.com/Department/HR/Equipment/action/id
Can I get the controllers to function in this manner? Is there someone to read so I can figure this out?
Thanks to those that read all this :)
I think I know what you're trying to do. Correct me if I'm wrong:
I built a project manager for myself in which I wanted the URLs to be more logical, so instead of using something like
http://domain.com/project/milestones/add/MyProjectName I could use
http://domain.com/project/MyProjectName/milestones/add
I added a custom route to the end (!important) of my routes so that it catches anything that's not already a route and treats it as a "variable route".
Router::connect('/project/:project/:controller/:action/*', array(), array('project' => '[a-zA-Z0-9\-]+'));
Whatever route you put means that you can't already (or ever) have a controller by that name, for that reason I consider it a good practice to use a singular word instead of a plural. (I have a Projects Controller, so I use "project" to avoid conflicting with it.)
Now, to access the :project parameter anywhere in my app, I use this function in my AppController:
function __currentProject(){
// Finding the current Project's Info
if(isset($this->params['project'])){
App::import('Model', 'Project');
$projectNames = new Project;
$projectNames->contain();
$projectInfo = $projectNames->find('first', array('conditions' => array('Project.slug' => $this->params['project'])));
$project_id = $projectInfo['Project']['id'];
$this->set('project_name_for_layout', $projectInfo['Project']['name']);
return $project_id;
}
}
And I utilize it in my other controllers:
function overview(){
$this->layout = 'project';
// Getting currentProject id from App Controller
$project_id = parent::__currentProject();
// Finding out what time it is and performing queries based on time.
$nowStamp = time();
$nowDate = date('Y-m-d H:i:s' , $nowStamp);
$twoWeeksFromNow = $nowDate + 1209600;
$lateMilestones = $this->Project->Milestone->find('all', array('conditions'=>array('Milestone.project_id' => $project_id, 'Milestone.complete'=> 0, 'Milestone.duedate <'=> $nowDate)));
$this->set(compact('lateMilestones'));
$currentProject = $this->Project->find('all', array('conditions'=>array('Project.slug' => $this->params['project'])));
$this->set(compact('currentProject'));
}
For your project you can try using a route like this at the end of your routes.php file:
Router::connect('/:groupname/:controller/:action/*', array(), array('groupname' => '[a-zA-Z0-9\-]+'));
// Notice I removed "/project" from the beginning. If you put the :groupname first, as I've done in the last example, then you only have one option for these custom url routes.
Then modify the other code to your needs.
If this is a public site, you may want to consider using named variables. This will allow you to define the group on the URL still, but without additional functionality requirements.
http://example.com/team/group:hr
http://example.com/team/action/group:hr/other:var
It may require custom routes too... but it should do the job.
http://book.cakephp.org/view/541/Named-parameters
http://book.cakephp.org/view/542/Defining-Routes
SESSIONS
Since web is stateless, you will need to use sessions (or cookies). The question you will need to ask yourself is how to reflect the selection (or not) of a specific department. It could be as simple as putting a drop down selection in the upper right that reflects ALL, HR, Sales, etc. When the drop down changes, it will set (or clear) the Group session variable.
As for the functionality in the controllers, you just check for the Session. If it is there, you limit the data by the select group. So you would use the same URLs, but the controller or model would manage how the data gets displayed.
// for all functionality use:
http://www.example.com/Team/action/id
http://www.example.com/Task/action/id
http://www.example.com/Equipment/action/id
You don't change the URL to accommodate for the functionality. That would be like using a different URL for every USER wanting to see their ADDRESS, PHONE NUMBER, or BILLING INFO. Where USER would be the group and ADDRESS, PHONE NUMBER< and BILLING INFO would be the item sets.
WITHOUT SESSIONS
The other option would be to put the Group filter on each page. So for example on Team/index view you would have a group drop down to filter the data. It would accomplish the same thing without having to set and clear session variables.
The conclusion is and the key thing to remember is that the functionality does not change nor does the URLs. The only thing that changes is that you will be working with filtered data sets.
Does that make sense?

Resources