Pointing a route to different controllers based on role - laravel

Let’s say we’re building an app which has two kinds of user - OfficeStaff and Engineer. We want them both to hit /dashboard as their main landing page after login, but they should both see completely different things.
We use the users table for both of them since they both log in the same way, and create a role field to determine who’s the OfficeStaff and who’s the Engineer.
When we create the routes, my first thought was to do this:
Route::group( [ ‘middleware’ => ‘role:office-staff’ ] , function () {
Route::get( ‘dashboard’, function() { return ‘Office staff dash’; } );
} );
Route::group( [ ‘middleware’ => ‘role:engineer’ ] , function () {
Route::get( ‘dashboard’, function() { return ‘Engineer dash’; } );
} );
However the problem is that Laravel doesn’t take middleware into account when compiling routes - it just looks at the path, so the second route overwrites the first one and we don’t have a dashboard route for the office staff.
Since the routes file is loaded before the middleware is run, the Auth middleware doesn’t load in the current user so we can’t dynamically namespace the route like this:
Route::get( ‘dashboard’, [ ‘as’ => ‘dashboard’, ‘uses’ => Auth::user()->role . ‘\DashboardController#dashboard’ ] );
The only other way that occurs to me is detecting it in the controller then creating a new controller instance of the appropriate sub-controller…
public function dashboard( Request $request ) {
App::make( ‘App\Controllers\’ . $request->user()->role . ‘DashboardController’ )->dashboard();
}
But that feels like we’re entering a world of hurt where we lose the ability to type hint inside controller methods, and we’re going to have a LOT of generic controller actions like this.
After all of this, I can't think of a good, clean way to set this up. Any ideas? :)

Related

Using route actions with new laravel route syntax

I have question about syntax which I can not find an answer for.
I have code in routes file:
Route::get(
'/something/{seoString}/{someMore}',
['as' => 'my_name', 'uses' => '\my\namespace\MyController#index', 'my_route_action' => 20]
);
And I would like to rewrite it using the new syntax for calling controller
Route::get(
'/something/{seoString}/{someMore}',
[MyController#::class, 'index'] // would like to use this new syntax
);
And it works fine, but how can I add the custom route action 'my_route_action'?
I know it's possible to wrap the routes with a group and add it this way:
Route::group(['my_route_action' => 20], static function () {
Route::get(
'/something/{seoString}/{someMore}',
[MyController#::class, 'index'] // would like to use this new syntax
);
);
But that's not what I'm looking for. I don't want to be adding one group for each route just to add the route action.
So I wanted to ask if it does exist something like ->addCustomAction() or how is this supposed to be done?
Unfortunately the route action is not a thing, and probably shouldn't be. Unsure what you're actually trying to achieve with that too.
If you're passing in GET data like a bit of data, you can do it through: {variable} so the URL would become the following:
Route::get('my-route-url/{model}/get', [MyController::class, 'methodName')->name('something')->middleware(['something'])
And in your controller, you dependency inject request if you're wanting to use that too, as well as the model:
public function methodName(Request $request, Model $model)
{
dd($request->all(), $model);
}
The "as" is the the name method. Middleware is still middleware.
If you're trying to do a Key/Pair bit of data, you need to use POST request and pass it in the data, which you can access via the $request->input('keyName') method in the controller.

Why InitializeTenancyByDomain is not applicable for the login process?

This is for Laravel 8.x with Jetstream/Livewire scaffold having Stancl/Tenancy. The initialization of tenant models or session settings not working right. Either I did not do it right or inbuilt problem.
The entire package was built as per instructions of Stencl/tenancy v3.x. I can see the dd(\App\User::all()) as per code outlined below
Route::middleware([
'web',
InitializeTenancyByDomain::class,
PreventAccessFromCentralDomains::class,
])->group(function (){
Route::get('/', function () {
dd(\App\User::all()); //can see all users models in tenants table
return view('welcomeTenant');
});
Route::get('/home', [
'middleware' => ['auth'],
'uses' => '\App\Http\Controllers\HomeController#index'
])->name('home');
});
This meant InitializeTenancyByDomain right to me.
When a login form is requested from tenant's domain eg. from rtbs.example.in, the encrypted session/cookie info is not stored in sessions table of tenant i.e. rtbs.sessions. When a login form is posted, it is looking for users in central domain (example.in) where users table is not present, hence the central.users table not exist error. As a result I get 419 error. I had disabled the csrf token verification temporarily to identify this problem.
This is the issue. Why the InitializeTenancyByDomain is not applicable for the login process? Could there be a fundamental setting wrong with me? Interestingly, the dd(\App\User::all()) if present anywhere else i.e. as show below
Route::middleware([
'web',
InitializeTenancyByDomain::class,
PreventAccessFromCentralDomains::class,
])->group(function (){
dd(\App\User::all()); //central-domain.users does not exist error
Route::get('/', function () {
return view('welcomeTenant');
});
Route::get('/home', [
'middleware' => ['auth'],
'uses' => '\App\Http\Controllers\HomeController#index'
])->name('home');
});
The same sql exception i.e. central-domain.users table does not exist is thrown. Only when present inside the Route::get('/'... i can see the correct models.
I had this problem with Laravel Fortify, which provides the backend to JetStream. It only became a problem when I changed the session driver to database from file - though this will be necessary for my deployment anyway.
The issue is that Fortify makes extensive use of dependency injection inside the constructor methods of their controllers. As described in the documentation for Tenancy, because of the lifecycle of a Laravel request the tenancy will not be available when these constructors run. This means they will default to the central connection and cause failure to login.
Because we can't change the way Fortify uses constructors, the solution is to change the middleware to be global and add a check for central domains into the initialization middleware:
Copy Stancl\Tenancy\Middleware\InitializeTenancyByDomain to App\Middleware\InitializeTenancyByDomain changing:
public function handle($request, Closure $next)
{
//Skip for central domains
if(in_array($request->getHost(), config('tenancy.central_domains'), true)){
return $next($request);
}
return $this->initializeTenancy(
$request, $next, $request->getHost()
);
}
App/Http/Kernel
use App\Http\Middleware\InitializeTenancyByDomain;
protected $middleware = [
// ...
InitializeTenancyByDomain::class,
];
Config/fortify
use Stancl\Tenancy\Middleware\PreventAccessFromCentralDomains;
'middleware' => [
'web',
PreventAccessFromCentralDomains::class,
],
Routes/tenant
Route::middleware([
'web',
PreventAccessFromCentralDomains::class,
])->group(function () {
//Your Routes
}
With this solution Fortify and Stancl Tenancy are now working well together with well separated databases, sesssions and logins.
I got it to work with Rory's answer. However, you need to set the correct namespace in App\Http\Middleware\InitializeTenancyByDomain.php:
namespace App\Http\Middleware; // << use this instead of Stancl\Tenancy\Middleware
// [..]
use Stancl\Tenancy\Middleware\IdentificationMiddleware; // <<
class InitializeTenancyByDomain extends IdentificationMiddleware
That package is very helpful, but it requires a lot of work to get all the pieces lined up and maintained. You've got the right middleware, including 'web' and the two tenancy bits, which will start the session and allow checks for CSRF.
I've had similar problems before. The biggest cause for me was that it was not finding the right 'central' path, and thus not initializing tenancy correctly.
Make sure your tenancy.php config file is showing the right central domain. Something like:
'central_domains' => [
'example.in' // No http:// here
],
Route's like next can issue bugs with database session store
Route::view('some')
You could use a feature called Universal Routes check the documentation here

Laravel Route Controller issue

I am trying to add a new route to my application and can't seem to get it to work. I keep getting a 404 error. It looks like the physical path is looking at the wrong directory. Currently looking at D:\Web\FormMapper\blog\public\forms but should be looking at D:\Web\FormMapper\blog\resources\view\layout\pages\forms.blade.php
My request URL:
http://localhost/FormMapper/ /works fine
http://localhost/FormMapper/forms /doesn't work
http://localhost/FormMapper/forms.php /No input file specified.
my FormsController:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class FormsController extends Controller
{
public function index()
{
return view('layouts.pages.forms');
}
}
My web.php:
Route::get('/', function () {
return view('layouts/pages/login');
});
Route::get('/forms', 'FormsController#index');
My folder structure looks like this:
My config/view.php
return [
'paths' => [
resource_path('views'),
],
'compiled' => env(
'VIEW_COMPILED_PATH',
realpath(storage_path('framework/views'))
),
];
you must use dot for this. In your controller change to this:
return view('layouts.pages.forms');
If your route only needs to return a view, you may use the Route::view method. Like the redirect method, this method provides a simple shortcut so that you do not have to define a full route or controller. The view method accepts a URI as its first argument and a view name as its second argument. In addition, you may provide an array of data to pass to the view as an optional third argument:
Route::view('/', 'layouts.pages.login');
Route::view('/forms', 'layouts.pages.forms', ['foo' => 'bar']);
Check docs
After tracking digging deeper I determined that the issue was that IIS requires URL rewrite rules in place for Laravel to work properly. The index.php and '/' route would work b/c it was the default page but any other pages wouldn't. To test this I used the
php artisan serve
approach to it. and everything worked properly. Unfortunately I am unable to do this in production so I needed to get it to work with IIS.

Laravel: Best way to implement dynamic routing in routes.php based on environment variable?

My aim is to roll out a big re-theming / re-skinning (including new URL routing) for a Laravel v5 project without touching the existing business logic (as much as possible that is).
This is my current approach:
I placed a APP_SKIN=v2 entry in my .env file
My app\Http\routes.php file has been changed as follows:
if (env('APP_SKIN') === "v2") {
# Point to the v2 controllers
Route::get('/', 'v2\GeneralController#home' );
... all other v2 controllers here ...
} else {
# Point to the original controllers
Route::get('/', 'GeneralController#home' );
... all other controllers
}
All v2 controllers have been placed in app/Http/Controllers/v2 and namespaced accordingly
All v2 blade templates have been placed in resources/views/v2
the rest of the business logic remains exactly the same and shared between the "skins".
My question: Is there a "better" way to achieve the above?. Please note that the idea here is to affect as few files as possible when doing the migration, as well as ensure that the admin can simply change an environment variable and "roll back" to the previous skin if there are problems.
Within app/Providers/RouteServiceProvider.php you can define your routes and namespaces, etc. This is where I would put the logic you talked about (rather than in the routes file):
protected function mapWebRoutes()
{
if (App::env('APP_SKIN') === 'v2') {
Route::group([
'middleware' => 'web',
'namespace' => $this->namespace,
], function ($router) {
require base_path('routes/web_v2.php');
});
} else {
// ...
}
}
This way, you can create separate route files to make it a bit cleaner.
Aside from that, I personally can't see a better solution for your situation than what you described as I'm guessing your templates want to vary in the data that they provide, which if that is the case then you will need new controllers - otherwise you could set a variable in a middleware which is then retrieved by your current controllers which could then determine which views, css and js are included. This would mean you would only need to update your existing controllers, but depending upon your current code - this could mean doing just as much work as your current solution.
Routes pass through Middleware. Thus you can achieve this by BeforeMiddleware as follows
public function handle($request, Closure $next)
{
// Get path and append v2 if env is v2
$path = $request->path();
$page = $str = str_replace('', '', $path); // You can replace if neccesary
// Before middleware
if (env('APP_SKIN') === "v2")
{
return $next($request);
}
else
{
}
}

Laravel route service provider not triggered for domain key

I’m creating a SaaS app (who isn’t?) and like most SaaS apps, I’ve taking the account subdomain approach. My routes file looks like this:
$router->group(['domain' => '{account}.example.com'], function($router)
{
$router->get('/', function()
{
return response('Hello, world.');
});
});
I then decided to add some route parameter validation and binding in my RouteServiceProvider file:
public function boot(Router $router)
{
parent::boot($router);
$router->pattern('account', '[a-z0-9]+');
$router->bind('account', function($subdomain)
{
return Account::whereSubdomain($subdomain)->firstOrFail();
});
}
However, these don’t actually seem to be triggered. I know this as I can put something like dd('here?') in the bind call, and it’s never triggered. I can also reduce my account pattern filter to something like [0-9]+ and it’ll still be matched if I include letters in the subdomain.
What am I doing wrong? How can I get route patterns and bindings to work on variables in the domain key of my route group?
Turns out moving any bindings to the map method (instead of the boot) method works, and pattern filters need to go inside the route group definition, like so:
$router->group(['domain' => '{account}.example.com'], function($router)
{
$router->pattern('account', '[a-z0-9]+');
$router->get('/', function()
{
return response('Hello, world.');
});
});
Not ideal, so any one knows how to have filter patterns be kept in my RouteServiceProvider class so they’re not littered in my routes file, then would love to hear from you.

Resources