DEV Community

Cover image for Laravel Jetstream database session with multiple user table
Hafiq Iqmal
Hafiq Iqmal

Posted on

Laravel Jetstream database session with multiple user table

If you are using Jetsream, you might notice that there is a feature called Browser Sessions. This feature allow the user to view the browser sessions associated with their account. Plus, the user may revoke other browser sessions other than the one being used by the device they are currently using.

So, what is the problem?

The problem is that, when multiple guard authentication happen, session is stored only based on user primary key in user_id column. Based on component Jetstream LogoutOtherBrowserSessionsForm , the logic is where if there are 2 guard with same id is stored and one of the guard user click revoke session in Jetstream, both of the session would be deleted. It would be nice if the session table accept polymorphic relationship

Figure 1

How about the solution?

So, i decide to came out a solution

  1. Create a custom session driver to override database session used by Laravel default database session manager
  2. Alter current session to accept polymorphic relation
  3. Implement polymorphic relation to LogoutOtherBrowserSessionsForm

Lets get started

If you are not using database driver for session, this article might not for you.

Create a custom session driver

So, let’s start by looking at \Illuminate\Session\DatabaseSessionHandler. You will notice that there is method addUserInformation to add user_id to the payload of session table. This is where we can extend this class and override this method to add our polymorphic relation.

Create a class name as DatabaseSessionHandler extend from \Illuminate\Session\DatabaseSessionHandler. Override addUserInformation and add to the payload with morph column. We might want to keep the parent method to keep the old session driver. Here the full snippet :-

<?php

class DatabaseSessionHandler extends \Illuminate\Session\DatabaseSessionHandler
{
    protected function addUserInformation(&$payload)
    {
        if ($this->container->bound(Guard::class)) {
            $payload['authenticable_id'] = $this->userId();
            $payload['authenticable_type'] = $this->container->make(Guard::class)->user()?->getMorphClass();
        }

        return parent::addUserInformation($payload);
    }
}
Enter fullscreen mode Exit fullscreen mode

Done extending the DatabaseSessionHandler. Now, registering the DatabaseSessionHandler is done through a provider, which is set up the same way as the built-in \Illuminate\Session\DatabaseSessionHandler. Im using name “database2” as a driver name. You may freely change the driver name as you wish

<?php


class SessionServiceProvider extends ServiceProvider
{
    public function boot()
    {
        \Session::extend('database2', function ($app) {
            return new DatabaseSessionHandler(
                $app['db']->connection($app['config']['session.connection']), 
                $app['config']['session.table'], 
                $app['config']['session.lifetime'], 
                $app
            );
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, the custom session database driver is now registered.

Create/Alter Session Migration Table

Lets start with by publishing a migration for session table if not exist

> php artisan session:table
Enter fullscreen mode Exit fullscreen mode

The content of the session would look like this

Schema::create('sessions', function (Blueprint $table) {
    $table->string('id')->primary();
    $table->foreignId('user_id')->nullable()->index();
    $table->string('ip_address', 45)->nullable();
    $table->text('user_agent')->nullable();
    $table->text('payload');
    $table->integer('last_activity')->index();
});
Enter fullscreen mode Exit fullscreen mode

You would notice this migration doesn’t come with polymorphic relationship. This is where we need to alter the table. Add morphs relation named it as authenticable

Schema::create('sessions', function (Blueprint $table) {
    $table->string('id')->primary();
    $table->nullableMorphs('authenticable'); // add this

    $table->foreignId('user_id')->nullable()->index();

    $table->string('ip_address', 45)->nullable();
    $table->text('user_agent')->nullable();
    $table->text('payload');
    $table->integer('last_activity')->index();
});
Enter fullscreen mode Exit fullscreen mode

I would suggest not to remove the user_id column because we shared session table. In case, you want to revert back to original session database driver, it wont be a problem. Unless you specify another table for new session driver.

In case you already have session table, you might want to alter the table. Just create another migration to alter the table

> php artisan make:migration alter_session_table --table=sessions
Enter fullscreen mode Exit fullscreen mode

You might need to delete all the existing session. Just add DB truncate before migration happen. You may follow like below :-

<?php

class AlterSessionTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        DB::table('sessions')->truncate();

        Schema::table('sessions', function(Blueprint $table) {
            $table->after('id', function (Blueprint $table ){
                $table->nullableMorphs('authenticable');
            });
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('sessions', function(Blueprint $table) {
            $table->dropMorphs('authenticable');
        });
    }
}

Enter fullscreen mode Exit fullscreen mode

Change session driver in env file

At this point, you can change session default driver to your new custom session driver.

...
QUEUE_CONNECTION=
SESSION_DRIVER=database2
...
Enter fullscreen mode Exit fullscreen mode

You may try to login and you will notice in your session driver table, the morph column started to filled in.

Figure 2

Customize LogoutOtherBrowserSessionsForm (Jetstream)

Let’s take a look at Jetstream LogoutOtherBrowserSessionsForm class.

Figure 3

The red line shows the logic used to display and revoke session in the browser session feature. As mention, the query is not handling multi table or multi guard which cause see session of others. Let’s create new livewire component and extend this class.

> php artisan make:livewire LogoutOtherBrowserSessionsForm
Enter fullscreen mode Exit fullscreen mode

Now alter the file and extend to Jetstream class

<?php

use Laravel\Jetstream\Http\Livewire\LogoutOtherBrowserSessionsForm as BaseLogoutOtherBrowserSessionsForm;

class LogoutOtherBrowserSessionsForm extends BaseLogoutOtherBrowserSessionsForm
{
   //
}
Enter fullscreen mode Exit fullscreen mode

Override both method deleteOtherSessionRecords and getSessionsProperty to meet the requirement. Adding extra query and fix some logic and it will look like this :-

<?php

namespace App\Http\Livewire;

use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Laravel\Jetstream\Http\Livewire\LogoutOtherBrowserSessionsForm as BaseLogoutOtherBrowserSessionsForm;

class LogoutOtherBrowserSessionsForm extends BaseLogoutOtherBrowserSessionsForm
{
    /**
     * Delete the other browser session records from storage.
     *
     * @return void
     */
     protected function deleteOtherSessionRecords()
     {
         if (!Str::contains(config('session.driver'), 'database')) {
             return;
         }

         DB::connection(config('session.connection'))
             ->table(config('session.table', 'sessions'))
             ->where('authenticable_id', Auth::user()->getAuthIdentifier())
             ->where('authenticable_type', Auth::user()->getMorphClass())
             ->where('id', '!=', request()->session()->getId())
             ->delete();
     }

    /**
     * Get the current sessions.
     *
     * @return \Illuminate\Support\Collection
     */
    public function getSessionsProperty(): \Illuminate\Support\Collection
    {
        if (!Str::contains(config('session.driver'), 'database')) {
            return collect();
        }

        return collect(
            DB::connection(config('session.connection'))
                ->table(config('session.table', 'sessions'))
                ->where('authenticable_id', Auth::user()->getAuthIdentifier())
                ->where('authenticable_type', Auth::user()->getMorphClass())
                ->orderBy('last_activity', 'desc')
                ->get()
        )->map(function ($session) {
            return (object) [
                'agent' => $this->createAgent($session),
                'ip_address' => $session->ip_address,
                'is_current_device' => $session->id === request()->session()->getId(),
                'last_active' => Carbon::createFromTimestamp($session->last_activity)->diffForHumans(),
            ];
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Ok now everything is set up.

Figure 4

Once verified, the browser session shows only the one i’m using logged in based on guard. Means that our custom session are working fine. 🤘

That’s it. Hope its help 😁. Thanks for your time.

References

https://jetstream.laravel.com/2.x/introduction.html

Top comments (0)