Laravel Jetstream database session with multiple user table
Tags: laravel, jetstream, php
Published: 2021-10-11 07:10:19

If you are using Jetsream, you might notice that there is a feature called [Browser Sessions](https://jetstream.laravel.com/2.x/features/browser-sessions.html#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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qlm1nqjd45pomjqr4z8u.png)


<br>
<br>
### 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);
    }
}
```

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
            );
        });
    }
}
```

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
```

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();
});
```

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();
});
```

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
```

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');
        });
    }
}

```

### 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
...
```

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

![Figure 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v5ebbkblbycgif6ue9hn.png)

### Customize LogoutOtherBrowserSessionsForm (Jetstream)
Let’s take a look at Jetstream `LogoutOtherBrowserSessionsForm class`.

![Figure 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ghxyrbpp9gnm8bgng0te.png)

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
```

Now alter the file and extend to Jetstream class

```
<?php

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

class LogoutOtherBrowserSessionsForm extends BaseLogoutOtherBrowserSessionsForm
{
   //
}
```

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(),
            ];
        });
    }
}
```

Ok now everything is set up.

![Figure 4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ewqc5m9my114k4hu9ds7.png)

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