14
votes

I'm experimenting with Middleware in my Laravel application. I currently have it set up to run on every route for an authenticated user, however, I want it to ignore any requests that begin with the setup URI.

Here is what my CheckOnboarding middleware method looks like:

public function handle($request, Closure $next)
{
    /** 
    * Check to see if the user has completed the onboarding, if not redirect.
    * Also checks that the requested URI isn't the setup route to ensure there isn't a redirect loop.
    */
    if ($request->user()->onboarding_complete == false && $request->path() != 'setup') {
        return redirect('setup');
    } else {
        return $next($request);
    }
}

This is being used in my routes like this:

Route::group(['middleware' => ['auth','checkOnboarding']], function () {
    Route::get('/home', 'HomeController@index');
    Route::get('/account', 'AccountController@index');

    Route::group(['prefix' => 'setup'], function () {
        Route::get('/', 'OnboardingController@index')->name('setup');
        Route::post('/settings', 'SettingsController@store');
    }); 
});

Now, if I go to /home or /account I get redirected to /setup as you would expect. This originally caused a redirect loop error hence why & $request->path() != 'setup' is in the Middleware.

I feel like this is a really clunky way of doing it, and obviously doesn't match anything after setup like the setup/settings route I have created.

Is there a better way to have this Middleware run on all routes for a user, but also set certain routes that should be exempt from this check?

5

5 Answers

16
votes

There's nothing wrong with what you're doing, however, I would suggest splitting your route groups up instead i.e.:

Route::group(['middleware' => ['auth', 'checkOnboarding']], function () {
    Route::get('/home', 'HomeController@index');
    Route::get('/account', 'AccountController@index');
});

Route::group(['prefix' => 'setup', 'middleware' => 'auth'], function () {
    Route::get('/', 'OnboardingController@index')->name('setup');
    Route::post('/settings', 'SettingsController@store');
});

Alternatively, have a parent group for your auth:

Route::group(['middleware' => 'auth'], function () {

    Route::group(['middleware' => 'checkOnboarding'], function () {
        Route::get('/home', 'HomeController@index');
        Route::get('/account', 'AccountController@index');
    });

    Route::group(['prefix' => 'setup'], function () {
        Route::get('/', 'OnboardingController@index')->name('setup');
        Route::post('/settings', 'SettingsController@store');
    });
});

This will also mean you can remove the extra condition in your middleware:

/**
 * Check to see if the user has completed the onboarding, if not redirect.
 * Also checks that the requested URI isn't the setup route to ensure there isn't a redirect loop.
 */
return $request->user()->onboarding_complete ? $next($request) : redirect('setup');

Hope this helps!

12
votes

You can utilize the Controller class for this with pretty spectacular results.

If you create a __construct function inside of HTTP/Controllers/Controller.php then you can declare middleware to run on every controller action and even declare exceptions as needed.

class Controller extends BaseController
{
  use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
  public function __construct(){
    $this->middleware('auth',['except' => ['login','setup','setupSomethingElse']]);
  }
}

Be careful not to put any of the standard index, store, update, destroy functions in the exception or you'll open up potential security issues.

6
votes

Since Laravel 7.7 you can use excluded_middleware like this:

Route::group(['middleware' => ['auth','checkOnboarding']], function () {
    Route::get('/home', 'HomeController@index');
    Route::get('/account', 'AccountController@index');

    Route::group([
      'prefix' => 'setup',
      'excluded_middleware' => ['checkOnboarding'],
], function () {
        Route::get('/', 'OnboardingController@index')->name('setup');
        Route::post('/settings', 'SettingsController@store');
    }); 
});
2
votes

There are 2 ways to go over this problem

  1. Try screening your routes in routes file web.php or api.php
  2. skip routes in middleware

In case of global middleware (middleware that you want to run before all routes), you should go with skipping routes in middleware.

For example:

//add an array of routes to skip santize check
protected $openRoutes = [
    'setup/*',
];

/**
 * Handle an incoming request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Closure  $next
 * @return mixed
 */
public function handle($request, Closure $next)
{
    if(!in_array($request->path(), $this->openRoutes)){
       //middleware code or call of function
    }       

    return $next($request);
}

For other middleware, you can easily skip in routes file and group routes based on your middleware.

For example:

Route::group(['middleware' => 'checkOnboarding'], function () {
        Route::get('/home', 'HomeController@index');
        Route::get('/account', 'AccountController@index');
    });

Route::group(['prefix' => 'setup'], function () {
    Route::get('/', 'OnboardingController@index')->name('setup');
    Route::post('/settings', 'SettingsController@store');
});
1
votes

Routes on which you dont want the middleware to run , simply put them outside of the function:

//here register routes on which you dont want the middleware: checkOnboarding
Route::group(['middleware' => ['auth','checkOnboarding']], function () {
     //routes on which you want the middleware
});