Laravel - Booting web middleware from an exception's render method - laravel

Found few answers on SO but most of them where kind of hacky so I decided to post anyway.
So I'm using Laravel 8 + InertiaJS and my app has a Billing/Plan logic.
Whenever an app user wants to create a resource, the resource's policy perform checks to ensure the user is under its plan allowance.
If the user reached the limit, a custom exception is returned.
The exception returns an Inertia view, which inherit the app's layout (containing a navbar with user details).
class PlanLimitReached extends Exception
{
public function render($request)
{
if ($request->expectsJson()) {
return response()->json([
'error' => 'PlanLimitReached',
'message' => 'You reached the limitation for your current billing plan, please upgrade your account.',
'documentation' => 'https://docs.company.tld'
], 403);
}
return Inertia::render('User/Billing/LimitReached');
}
}
The issue is that component does not render because user variables are not set. It's expected behaviour as you would typically have a template that do not include auth-related logic specifically for 4XX & 5XX errors pages.
But I would rather have a page directly integrated in my app, so I need a way to force this specific exception to force loading the web middleware (exactly what is discussed in https://github.com/laravel/ideas/issues/676).
Unfortunately solutions I found are:
Requiring the web middleware in the global middleware, which is not really feasible as it would mess up my API routes
Creating a route for the error page and then having the exception redirect to this page, it would work but I find this solution quite ugly
Creating a "guest" layout without auth logic for exception pages, pretty ugly IMO
TLDR: Looking for a way to load web middleware from an exception's render method to be able to use auth()->user() in my templates, without redirects or requiring the web middleware globally.

Related

Where to check if an User is logged in in a Laravel Application?

I've been using your advice and View::sharing all of my important data to all views. However, there is one issue I have encountered.
This code:
if(!Auth::guest()){
$user=Auth::user()->id;
}
else $user=0;
$temp=DB::select('query');
View::share('cartnumber', count($temp));
View::share('cartitems', $temp);
doesn't work when put in AppServiceProvider. Or better, it always sets $user=0, even if I am logged in. I thought it is because AppServiceProvider's boot function executes before the site checks if someone is logged in.
I then tried to use a BaseController with a construct function but that doesn't work either. The only solution that seems to work correctly is putting the code in every single Controller for every view! That actually works, which kind of confirms my theory.
But is there anywhere I can put this code without having to copy/paste it in every single Controller? Thanks in advance!
You'd likely want to put this code later in the request life cycle to guarantee an auth user because as others have mentioned middleware/session code has not occured during this part of the framework booting up. You could use a service class to call in all your controllers to avoid the copy pasting. Or If you'd like to achieve this using code in your service provider you could use a View Composer instead of a share this allows you to define a callback/or class that will be called right before the view is returned
view()->composer(['/uri-that-needs-data'], function ($view) {
if (Auth::check()) {
$cart = DB::query(...)->get();
$view->with('cartitems', $cart);
}
});
Check out https://laravel.com/docs/5.7/views#view-composers for more details.
Auth::user() will be empty until the session middleware has run.
The reason you can't access the user inside your service provider is because that code is run during the "bootstrapping" phase of the application lifecycle, when it's doing things like loading filesystem or cache drivers, long before the request is sent through response handlers (including middleware).
Once the application has been bootstrapped and all service providers
have been registered, the Request will be handed off to the router
for dispatching. The router will dispatch the request to a route or
controller, as well as run any route specific middleware.
Source: https://laravel.com/docs/5.7/lifecycle
If you don't want to copy/paste that code everywhere, then one place to put it is in custom route middleware. You can list it after the auth middleware to guarantee a logged-in user.
Edit: View composers are another really good option, as suggested by #surgiie. The reason these can be set up inside a service provider (unlike your example) is because the view composer registers a callback, but doesn't execute it until a much later stage in the application lifecycle.

Laravel 5.7: User Not Set When Throwing NotFoundHttpException

I am overriding the rendering of a couple of exceptions in the App\Exceptions\Handler so I can return custom error pages. However, on some of the exceptions the auth()->user() is not set.
In the render-method, I am just dumping the user like this:
public function render($request, Exception $exception)
{
dd(auth()->user());
return parent::render($request, $exception);
}
When the exceptions are of the type NotFoundHttpException, the user isn't set. What would be the best solution for getting the user in these exceptions?
Here's the solution, but read on for a different recommendation.
By default, Laravel's authentication runs via the session. The session is explicitly started in the web middleware group.
If a route is not wrapped in the web middleware group (as a non-existent / 404 route would be), the middleware doesn't run, and the session isn't started. Therefore Laravel can't know if there is a session user.
Best solution: define a fallback route which was built for exactly this purpose.
Another solution, with gotchas: move the StartSession middleware into the global group that runs on every request.
/app/Http/Kernel.php:
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* #var array
*/
protected $middleware = [
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class,
// +++ MOVED SESSION START HERE BELOW +++
\Illuminate\Session\Middleware\StartSession::class,
];
But, should you...?
First, there's a reason the session doesn't load on every request by default: it creates extra overhead that might not be needed on every request. For example, if your website is configured to always use Laravel as the request handler, every 404 will route through Laravel. And there are a lot - crawlers and bots are constantly scouring the web for known insecure web paths. Take a look at your access logs.
Also, if you create self-contained error pages that don't depend on external application logic, you can reuse them at the server level. If Apache or Nginx throws an error, Laravel won't be there at all to dictate the output.
TL;DR: You can enable sessions on 404 and other pages, but understand the trade-offs. Personally, I recommend avoiding application logic on error pages.
Try:
Auth::user()
It should work the same way. Make sure you're including the Auth library.

Laravel 5.1 use session to restrict direct access using urls users based on user role

I have 2 laravel projects, 1 for the front end where i m using html css angularjs. The second for api controllers. I call using http post and get the api controllers functions using angularjs to get content data.
In the front end i have a menu this menu appears differently based on user role, if admin or no.
This is done. My problem is the access for views using the url in the browser.
So I have a query where I get for each user what modules in the menu can he see. Now I'm putting the result in Laravel session.
$menu = DB::select menu by user id ... //Getting menu query based on user if admin or no
session(["menu" => $menu);
return session('menu');
I'm getting the results and the menu is showing good in the website based on the logged user if he s admin or no.
Now, to solve the direct url access issue, I want to use this session and compare the url to this session, if the url exists in the session i will let him access, if no i will redirect him to somewhere.
any idea?
I would strongly suggest looking at the Laravel documentation on Authorization before going too far down a custom implementation:
https://laravel.com/docs/5.1/authorization
Without knowing more about how your front-end and back-end applications interact with each other, it is a little difficult to get into speciifics but i shall do my best.
Each page returned by Laravel has access to a Request object which contains information about the request which returned the page. You can access this Request and its assocaited Route using Laravels helper functions (if you are not passing it to the view already). The getPrefix() method will return the root relative url which you can then use as you see fit. For example:
// Return and store the URL as a string
$url = request()->route()->getPrefix();
// Check your session for the URL/s you want to allow and compare to the stored URL
if (session()->get('URL') == $url) {
// User is allowed access to the page
// Do something ...
} else {
// User is not allowed access to this page
// Redirect back or to a route of your choice
return redirect()->back();
}
I hope this gives you some ideas, good luck!

Laravel Middleware change response content

For my application I am having multiple user roles and a custom maintenance mode. If the site is in maintenance mode, then depending on the user's role will limit their access to certain pages.
So when "normal" users access the forums, they should instead see a different view saying that the site is in maintenance mode; whereas when "admin" users access the forums they should be able to see the forums.
public function handle($request, Closure $next) {
if(Auth::user()->role->maintenance_mode != 1) {
// They do not have access during maintenance mode,
// so change the response to show a different view.
}
// They do have access during maintenance mode,
// so continue the request.
return $next($request);
}
Is it possible to have the same route (e.g. /forums) but to show a different view, changed by the middleware.
It is possible, yes, however each middleware calls $next($request) which may point to another middleware before the route, therefore returning a view may not be the best idea. If you do want to do it, then you need to do something like: return new Response(view('maintanance')); and don't forget to include use Illuminate\Http\Response; in the header of your Middleware class.
In your case, what I would do is have a route that returns a view such as /maintainance (preferably with a name) and then in the middleware, return a redirect to the route return redirect()->route('maintanance'); within the if.
You could also throw a HttpException with the status code of 503 to have the application invoke Laravel's built in maintenance mode within that first if.

Grails Request Map custom errors

I've got a websoftware done with Grails that uses request maps from Spring Security.
I am using them for security reasons, so that the user can't access admin areas but also it's used for our business model. We've got two different user roles. One is premium and one is premium plus, while PREMIUM_PLUS > PREMIUM.
The premium plus user can access some more pages than the premium user can. If a premium user wants to access a page that can only get called by a premium plus user, there will be a error like 'no access', but I want a custom message like 'Upgrade now to premium plus'.
I could easily edit the template for all request maps errors but there are also some restricted pages nobody should see where a message like 'no access' is perfect for.
Is there any possibility to do this with two different error pages?
Thank you.
The LoginController have the action denied that shows the denied.gsp. You can customize your controller action to check if you need to display the upgrade page or the denied page.
class LoginController {
def denied() {
if (springSecurityService.isLoggedIn() &&
authenticationTrustResolver.isRememberMe(SCH.context?.authentication)) {
// have cookie but the page is guarded with IS_AUTHENTICATED_FULLY
redirect action: 'full', params: params
} else {
//implement a method checking whatever you need to define that will display the upgrade
if(mustShowUpgrade()) {
render view: 'upgrade'
}
}
}
}

Resources