Identify Route's Group in Middleware - laravel

My ultimate objective is to limit accesses to the group of routes by validating permissions provided to the user.
These target 'group of routes' have ONE COMMON PARENT GROUP and may have zero or more sub-groups, such that, if access to these target 'group of routes' is permitted/accessible to the user then, all its sub-route groups are also accessible to the user.
To achieve this, I believe I need to differentiate these target group of routes by any uniqueString/parameter in middleware, which is indeed answered here.
But, I want to generalize this further, by applying middleware to common SINGLE PARENT GROUP of all these target group of routes and identify these target group of routes by any means in the middleware.
So, my question is how do I identify/differentiate these target group of routes in the middleware? Is there any way to do so?
Sample Code of what I am trying to describe:
Route::group(['prefix' => 'singleParent','middleware' => 'permissionMiddleware'], function (){
Route::group(['prefix' => 'target-group-1', 'groupUniqueString' => 'tsg1'], function (){
Route::group(['prefix' => 'sub-group-1.1'], function (){
});
Route::group(['prefix' => 'sub-group-1.2'], function (){
});
});
Route::group(['prefix' => 'target-group-2', 'groupUniqueString' => 'tsg2'], function (){
Route::get('route-1','Controller#method-of-Route1');
});
});

So, to specify a route group in your middleware to handle some actions, you can do it in this way :
Route::group(['prefix' => 'singleParent','middleware' => 'permissionMiddleware'], function (){
Route::group(['prefix' => 'target-group-1', 'as' => 'tsg1.'], function (){
//...
});
});
This will generate route names with the prefix : tsg1
Now in your middleware you can do like this to get the route group :
function getCurrentRouteGroup() {
$routeName = Illuminate\Support\Facades\Route::current()->getName();
return explode('.',$routeName)[0];
}
Updated
and to check :
if ($request->route()->named('name')) {
//
}
return $next($request);
Or in another approach you can achieve :
To get the prefix of a route group you can do something like this :
$uri = $request->path();
// this will give you the url path like -> if this is the url :
// http://localhost:8000/foo/bar you will get foo/bar
And then :
$prefix = explode('/',$uri)[0];
// and you will get 'foo'
Let me know if this works for you.

Related

Laravel Route: Multiple Route Group With Prefix and Route Model Binding

Hello wonderful people of SO!
I have a problem about Laravel Route which I cannot solve.
In User.php model I use getRouteKeyName() function
public function getRouteKeyName()
{
return 'user_name';
}
And also in Post.php model
public function getRouteKeyName()
{
return 'uuid';
}
In users table, 1 have one record
|----------------------------|
| id | ... | user_name | ... |
| 1 |-----| #simple |-----|
In posts table
|------------------------------------|
| id | ... | uuid | ... |
| 1 |-----| abcd-123-efg-456 |-----|
In route (web.php)
// for post (key: uuid)
Route::group(['prefix' => '{post}'], function () {
Route::get('/', function (Post $post) {
return $post;
});
});
// for users (key: user_name)
Route::group(['prefix' => '{user}'], function () {
Route::get('/', function (User $user) {
return $user;
});
});
Then let say we visit url: www.example.test/#simple/
In debugbar, I see query:
select * from posts where uuid = '#simple' limit 1
What I have tried
[#1] I put where clause in route groups for posts and users
Route::group([
'prefix' => '{post}',
'where' => [
'post' => '^[a-zA-Z0-9-]{36}$' // I'm not Regex professional
]
], function () {
Route::get('/', function (Post $post) {
return $post;
});
});
Route::group([
'prefix' => '{user}',
'where' => [
'user' => '^(#)[a-zA-Z0-9]$' // I'm not Regex professional
]
], function () {
Route::get('/', function (User $user) {
return $user;
});
});
So let's try again visit the url: www.example.test/#simple
What i got, 404
[#2] I deleted the getRouteKeyname in both User and post model
revisit url: www.example.test/#simple, still got 404
[#3] I tried to put Route Model Binding Column Name
Route::group([
'prefix' => '{post:uuid}', // This is what I changed
], function () {
Route::get('/', function (Post $post) {
return $post;
});
});
Route::group([
'prefix' => '{user:user_name}', // This is what I changed
], function () {
Route::get('/', function (User $user) {
return $user;
});
});
Still, query result is same: > select * from posts where uuid = '#simple' limit 1
What I want to achieve
Let say we visit url: www.example.test/#simple
Fetch a user with user_name is #simple or if the user is not exist, return 404
And also same for with posts
We visit url: www.example.test/abcd-1234-efgh-5678
Fetch a post with uuid is abcd-1234-efgh-5678 or 404 if not exist
Question:
[#1] How to tell Laravel Route: that I have 2 Route groups with different Model Binding? Sorry if this question is kinda confusing, cause my english is not really good
[#2] Have I implement Best practice for route groups and route model binding in Laravel?
Thanks in advance!
What is the result you intend to obtain?
If you are doing what I think you're doing (trying to see what's inside the post), you need to return something like $post->content (replace content with the column you want to get), you may even want to make a view and make the output nicer, plus use a controller for more processing.
As for route model binding, you can refer to this, both methods, using table:column and using getRouteKeyName are fine, however, the first one doesn't change the default column, and if you use {user} for another route, it will still use the ID column, however, the second one changes the default value, if you use {user} for another route, it will use the column you specified.
Also, you should use something like user/{user:user_name} and post/{post:uuid} instead of just {user:username} and {post:uuid}, as you have said, it won't know which route you're using. The uri has to be different.
Routes are evaluated in the order you put them, meaning that the second route with {post:uuid} will override the route with {user:username} since they have the same kind of uri, that is, they both consist of 1 wildcard and nothing else. To solve this, you simply have to make their uri different by adding a static part, for example, add post/ before {post:uuid} and/or add user/ before {user:user_name} like the example below:
Route::group([
'prefix' => 'post/{post:uuid}',
], function () {
Route::get('/', function (Post $post) {
return $post;
});
});
Route::group([
'prefix' => 'user/{user:user_name}',
], function () {
Route::get('/', function (User $user) {
return $user;
});
});
To make it very clear, your 2 routes have the same uri of 1 wildcard and nothing else, thus, the last one that appears with this uri will override all the previous routes with the same uri. Meaning that all the previous routes with this same uri before this will be treated like they don't exist, and when you go to a path with the uri in the format of /[insert something here], it fits into the format of having 1 wildcard and it will only go to the last one you specified, that is, the one for posts.
Since the route for users is declared before the one for posts and they share the same uri, only the one for posts will be used. Even when you are trying to find the user, it still uses the route for posts, if no such "post" with a uuid same as the user_name you provided exists, it will still return an error even when there is indeed such user with such username.
Also, you don't need a route group if there's simply 1 route, though it would be more readable and convenient if you're going to add more routes to the group in the future.
As far as I could understand your problem, here are the changes you need to make and it will work,
routes/web.php
Route::group([
'prefix' => 'post/{post:uuid}'
], function () {
Route::get('/', function (Post $post) {
return $post;
});
});
Route::group([
'prefix' => 'user/{user:user_name}'
], function () {
Route::get('/', function (User $user) {
return $user;
});
});
Regular Expression that you use above just does filter the {argument} and check if {argument} is alphanumeric basically, in above both cases it works the same except in user_name it also allows '-'

Localization in laravel

I designed a site with Laravel. now I want add new language to it.I read laravel document . It was good but I have a problem.suppose I have a page that show detail of products so I have a route like mysite.com/product/id that get product's id and show it.also I have a method in controller like
public function showProduct($id){
...
}
If I add new Language , the route will change to this: mysite/en/product/id
now I must change my method because now two parameter send my method.something like this :
public function showProduct($lang,$id){
...
}
So two problems arise:
I must change all method in my site which is time consuming
I do not need language parameter in methods because I set $locan via middleware
pay attention that I do not want remove for example en from my URL (because of SEO)
Open your RouteServiceProvider and say that language parameter actually is not a parameter, it's a global prefix.
protected function mapWebRoutes()
{
Route::group([
'middleware' => 'web',
'namespace' => $this->namespace,
'prefix' => Request::segment(1) // but also you need a middleware about that for making controls..
], function ($router) {
require base_path('routes/web.php');
});
}
here is sample language middleware, but it's need to be improve
public function handle($request, Closure $next)
{
$langSegment = $request->segment(1);
// no need for admin side right ?
if ($langSegment === "admin")
return $next($request);
// if it's home page, get language but if it's not supported, then fallback locale gonna run
if (is_null($langSegment)) {
app()->setLocale($request->getPreferredLanguage((config("app.locales"))));
return $next($request);
}
// if first segment is language parameter then go on
if (strlen($langSegment) == 2)
return $next($request);
else
// if it's not, then you may want to add locale language parameter or you may want to abort 404
return redirect(url(config("app.locale") . "/" . implode($request->segments())));
}
So in your controller, or in your routes. you don't have deal with language parameter
Something like
Route::group(['prefix' => 'en'], function () {
App::setLocale('en');
//Same routes pointing to the same methods...
});
Or
Route::group(['prefix' => 'en', 'middleware' => 'yourMiddleware'], function () {
//Same routes pointing to the same methods...
});

How to use multiple method in single route in laravel

I want to use more than one method in a single route using laravel. I'm try this way but when i dd() it's show the plan string.
Route::get('/user',[
'uses' => 'AppController#user',
'as' => 'useraccess',
'roles'=> 'HomeController#useroles',
]);
When i dd() 'roles' option it's show the plan string like this.
"roles" => "HomeController#useroles"
my middleware check the role this way.
$actions=$request->route()->getAction();
$roles=isset($actions['roles'])? $actions['roles'] : null;
The simplest way to accept multiple HTTP methods in a single route is to use the match method, like so:
Route::match(['get', 'post'], '/user', [
'uses' => 'AppController#user',
'as' => 'useraccess',
'roles'=> 'HomeController#useroles',
]);
As for your middleware, to check the HTTP request type, a tidier way would be:
$method = request()->method();
And if you need to check for a specific method:
if (request()->isMethod('post')) {
// do stuff for post methods
}
Here's how you can do multiple methods on a single route:
Route::get('/route', 'RouteController#index');
Route::post('/route', 'RouteController#create');
Route::put('/route', 'RouteController#update');
/* Would be easier to use
* Route::put('/route/{route}', 'RouteController#update');
* Since Laravel gives you the Model of the primary key you've passed
* in to the route.
*/
Route::delete('/route', 'RouteController#destroy');
If you've written your own middleware, you can wrap the routes in a Route::group and apply your middleware to those routes, or individual routes respectively.
Route::middleware(['myMiddleware'])->group(function () {
Route::get('/route', 'RouteController#index');
Route::post('/route', 'RouteController#create');
Route::put('/route', 'RouteController#update');
});
Or
Route::group(['middleware' => 'myMiddleware'], function() {
Route::get('/route', 'RouteController#index');
Route::post('/route', 'RouteController#create');
Route::put('/route', 'RouteController#update');
});
Whichever is easier for you to read.
https://laravel.com/docs/5.6/routing#route-groups

How to add dynamically prefix to routes?

In session i set default language code for example de. And now i want that in link i have something like this: www.something.com/de/something.
Problem is that i cant access session in routes. Any suggestion how can i do this?
$langs = Languages::getLangCode();
if (in_array($lang, $langs)) {
Session::put('locale', $lang);
return redirect::back();
}
return;
Route::get('blog/articles', 'StandardUser\UserBlogController#AllArticles');
So i need to pass to route as prefix this locale session.
If you want to generate a link to your routes with the code of the current language, then you need to create routes group with a dynamic prefix like this:
Example in Laravel 5.7:
Route::prefix(app()->getLocale())->group(function () {
Route::get('/', function () {
return route('index');
})->name('index');
Route::get('/post/{id}', function ($id) {
return route('post', ['id' => $id]);
})->name('post');
});
When you use named routes, URLs to route with current language code will be automatically generated.
Example links:
http://website.com/en/
http://website.com/en/post/16
Note: Instead of laravel app()->getLocale() method you can use your own Languages::getLangCode() method.
If you have more questions about this topic then let me know about it.
Maybe
Route::group([
'prefix' => Languages::getLangCode()
], function () {
Route::get('/', ['as' => 'main', 'uses' => 'IndexController#index']);
});

Laravel Conditional route filter

Hey guys could you please help me? This one is driving me crazy...
Let's say that I have a method for checking if the user is an admin or not:
public function isAdmin()
{
return Auth::user()->role === 'admin';
}
Then I attach it to a route filter:
Route::filter('admin', function($route, $request)
{
if ( ! Auth::user()->isAdmin()) {
Notification::error('No permission to view this page!');
return Redirect::back();
}
});
Now, I just pass it to the route group
Route::group(array('before' => 'admin'), function()
{
Route::post('users/{id}/update_password', 'UserController#update_password');
Route::post('users/{id}/delete', 'UserController#force_delete');
Route::delete('users/{id}', array('as' => 'users.destroy', 'uses' => 'UserController#destroy'));
Route::post('users/{id}/restore', 'UserController#restore');
Route::get('users/create', array('as' => 'users.create', 'uses' => 'UserController#create'));
Route::post('users', array('as' => 'users.store', 'uses' => 'UserController#store'));
Route::get('users/{id}/edit', array('as' => 'users.edit', 'uses' => 'UserController#edit'));
Route::put('users/{id}', array('as' => 'users.update', 'uses' => 'UserController#update'));
});
The question here is how do I allow a user to bypass this filter if for example he's trying to update it's own profile page an obviously he's not and admin?
I just want to block all access to the users routes for nonadmins but allow the user to edit/update etc on his own profile but allow the admin to do that too.
Could you please point me to the right direction?
You can get the related request segment to check it in your filter:
Route::filter('admin', function($route, $request)
{
if ( ! Auth::user()->isAdmin() && Auth::user()->username !== Request::segment(2)) {
Notification::error('No permission to view this page!');
return Redirect::back();
}
});
There are a few ways to do this, but having a filter that checks the request segments against the currently authenticated user isn't the best way.
Choice Number 1
You simply check that a user is auth'd (use the auth filter), and then in the controller itself you check whether or not the user is an admin, and/or it's their profile.
Choice Number 2
Define a secondary sets of routes specifically for a user modifying their own profile, that doesn't follow the /user/{id}/* pattern.
Route::group(['before' => 'admin'], function() {
// admin routes here
}
Route::group(['prefix' => '/me'], function() {
Route::post('/update_password', 'UserController#update_password');
Route::post('/delete', 'UserController#force_delete');
// etc
}
This would mean that to edit their own profile, they could simply go to /me/edit rather than /user/{id}/edit. To avoid issues like repeating the same code, or errors because an argument is missing, you could do something like this in your controller.
private function getUserOrMe($id)
{
return $id !== false ? User::find($id) : Auth::user();
}
public function edit($id = false)
{
$user = $this->getUserOrMe($id);
}
I recently used this particular method for an API. Sure it requires defining the routes again, but providing that you've set them up with groups that make use of the prefix option, it's a copy and paste job, plus, there are routes an admin would have that a user wouldn't.
Either way, filters weren't intended to do complex logic, but rather, to provide a level of base logic and protection for routes. Logic that identifies whether the current uri is that of the currently logged in user, is something better handled in a controller.

Resources