Custom DatabaseSessionHandler in Laravel 8 - php

I'm trying to make a custom DatabaseSessionHandler but it doesn't work as expected.
The idea is to make the table sessions polymorphic to allow the session from multiple models.
(To be honest, I'm not even sure it's possible)
But even before changing the table to be polymorphic, I'm trying to add a custom driver in order to manipulate the sessions.
The issue seems that my DatabaseSessionHandler, is not correctly called when I try to sign in.
config/auth.php
'guards' => [
'web' => [
'driver' => 'custom-session',
'provider' => 'users',
],
'screen' => [
'driver' => 'custom-session',
'provider' => 'screens',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => Domain\User\Models\User::class,
],
'screens' => [
'driver' => 'eloquent',
'model' => Domain\Screen\Models\Screen::class,
],
]
AuthServiceProvider.php
public function boot()
{
//This is how the session is normally registered: https://github.com/laravel/framework/blob/8.x/src/Illuminate/Session/SessionManager.php#L83
Session::resolved(function ($session) {
$session->extend('screen-session', function ($app) {
$table = $app['config']['session.table'];
$lifetime = $app['config']['session.lifetime'];
$connection = $app['db']->connection($app['config']['session.connection']);
return new \Support\Session\DatabaseSessionHandler($connection, $table, $lifetime, $app);
});
});
// This is how the driver "session" is normally registered: https://github.com/laravel/framework/blob/8.x/src/Illuminate/Auth/AuthManager.php#L121
Auth::resolved(function ($auth) {
$auth->extend('custom-session', function ($app, $name, array $config) {
$provider = Auth::createUserProvider($config['provider']);
$guard = new SessionGuard($name, $provider, $app->session->driver('screen-session'));
if (method_exists($guard, 'setCookieJar')) {
$guard->setCookieJar($this->app['cookie']);
}
if (method_exists($guard, 'setDispatcher')) {
$guard->setDispatcher($this->app['events']);
}
if (method_exists($guard, 'setRequest')) {
$guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
}
return $guard;
});
});
}
DatabaseSessionHandler.php
This is currently a copy/past of the existing one that is used by session except the namespace. https://github.com/laravel/framework/blob/8.x/src/Illuminate/Session/DatabaseSessionHandler.php
I don't have any error message.
When I try to sign in (auth()->guard('web')->login($user))
It validate the login
I know it uses my DatabaseSessionHandler to destroy the current
session in the table.
The session ID is regenerated and I'm not logged in
An another scenario;
I try to sign in with the "remember me"
It validate the login
Somehow, the session is updated in the database with the current user_id but it doesn't seems that my DatabaseSessionHandler have been used
I'm logged in
Removing the screen from the guards and providers to only keep web and users, doesn't change anything.

Finally solved it. As always, it's a simple mistake.
I add to change in .env file
SESSION_DRIVER=screen-session
And now it's working as expected.
Now, also to answer my another question
The idea is to make the table sessions polymorphic to allow the session from multiple models. (To be honest, I'm not even sure it's possible)
From my quick tests it looks realizable. Here's what I did;
create_sessions_table.php
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->nullableMorphs('authenticable');
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->text('payload');
$table->integer('last_activity')->index();
});
DatabaseSessionHandler.php
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Session\DatabaseSessionHandler as BaseDatabaseSessionHandler;
class DatabaseSessionHandler extends BaseDatabaseSessionHandler
{
/**
* Get the currently authenticated user's type.
*
* #return mixed
*/
protected function userType()
{
$user = $this->container->make(Guard::class)->user();
return optional($user)->getMorphClass();
}
/**
* Add the user information to the session payload.
*
* #param array $payload
* #return $this
*/
protected function addUserInformation(&$payload)
{
if ($this->container->bound(Guard::class)) {
$payload['authenticable_id'] = $this->userId();
$payload['authenticable_type'] = $this->userType();
}
return $this;
}
}
Note: If you're using Laravel jetstream, you will probably need a custom LogoutOtherBrowserSessionsForm livewire/inertiajs component since this one is based and hardcoded with the database session and looks for user_id column

Related

Additive checks with jwt-auth in users_groups table

In Laravel 5.8 app using tymon/jwt-auth 1.0.0
I have users_groups table and I need for logged user for some controller to make check if inside of some group.
For this in routes/api.php I have :
Route::group(['middleware' => 'jwt.auth', 'prefix' => 'manager', 'as' => 'manager.'], function ($router) {
Route::get('users_of_event_selection/{event_id}', 'API\ManagerController#users_of_event_selection');
Route::post('add_user_to_event', 'API\ManagerController#add_user_to_event');
...
I app/Http/Controllers/API/ManagerController.php I added checks:
public function __construct()
{
$this->middleware('jwt.auth', ['except' => []]);
$request = request();
$this->requestData = $request->all();
$loggedUser= Auth::guard('api')->user();
$userGroupsCount = UsersGroups
::getByUserId($loggedUser->id)
->getByGroupId([ACCESS_ROLE_ADMIN,ACCESS_ROLE_MANAGER])
->count();
if($userGroupsCount == 0) {
return response()->json(['error' => 'Unauthorized'], 401);
}
}
But
$userGroupsCount == 0
the code above does not work as I expected and my control's method returns valid data. I suppose I can make
small function and to call it in top on any control's method, but if that ig good way? If jwt-auth has any way to extend
additive checks ?
Thanks!

Laravel: Validation unique on update still failing all the time

I've an update form that contains an image and other data to be updated I changed the default route key to use the name instead of the default key which is the ID and I made a separate form request to validate my requests It works fine when posting new record unfortunately it keeps failing with the name field which is unique field; I've checked all threads on github and in stackoverflow with no use although I have the same project in laravel 5.5 and it works fine and now I'm stuck with laravel 6
hereis my form
let data = new FormData();
data.append('name', this.channel.name);
data.append('base_color', this.channel.baseColor);
data.append('complementary_color', this.channel.complementaryColor);
if (this.file){
data.append('avatar', this.file);
}
data.append('_method', 'PUT');
axios.post(`/dashboard/channels/${this.channel.name}`, data).then(resp => {
this.$parent.$emit('channel_updated', resp.data);
}).catch(error => {
flash(error.response.data, 'danger', 'backEndStyle');
});
and here is my route
Route::resource('/dashboard/channels', 'ChannelController');
and here is my form request
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ChannelRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'name' => 'required|unique:channels,name,'. $this->id,
'base_color' => 'required',
'complementary_color' => 'required',
];
}
}
here is my controller for update method
public function update(Channel $channel, ChannelRequest $request)
{
$channel->update([
'name' => $request->name,
'bg_base_color' => $request->base_color,
'bg_complementary_color' => $request->complementary_color,
]);
return response($channel->fresh(), 200);
}
Use the ignore constraint to ignore the current model when validating unique
public function rules()
{
return [
'name' => ['required', Rule::unique('channels')->ignore($this->route('channel'))],
'base_color' => 'required',
'complementary_color' => 'required',
];
}

localization Faker\Factory does not work in laravel

I have a Post Model with these fields :
post_id
post_title
post_content
post_content_full
author
Now I want to use laravel sedders and model factories to create fake fa_IR localized data and insert to posts table.
For that I wrote this in database/factories/ModelFactory.php:
$factory->define(App\Post::class, function (Faker\Generator $faker) {
return [
'post_title' => $faker->sentence,
'post_content' => $faker->paragraph,
'post_content_full' => $faker->paragraph(3),
'author' => $faker->name
];
});
Then I created a PostsTableSeeder class like this :
use Illuminate\Database\Seeder;
class PostsTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* #return void
*/
public function run ()
{
factory(App\Post::class, 5)->create();
}
}
And in AppServiceProvider.php added below codes to register function :
$this->app->singleton(FakerGenerator::class, function () {
return FakerFactory::create('fa_IR');
});
But After running the seed , laravel uses default locale (en_US) and ignores fa_IR.
I do not know what else to do.
Update:
Even I changed in DEFAULT_LOCALE const vendor/fzaninotto/faker/src/Faker/Factory.php to fa_IR Nothing changed.
Not all faker methods are supported in every language, from what a quick lookup of the documentation says, the Company and Address provider are supported in the fa_IR localization
Try this way
$factory->define(App\Post::class, function () {
$faker = Faker\Factory::create('fa_IR');
return [
'post_title' => $faker->sentence,
'post_content' => $faker->paragraph,
'post_content_full' => $faker->paragraph(3),
'author' => $faker->name
];
});
You need to change the faker locale in your app config file.
First run this
php artisan make:factory PostFactory
Do like this
$faker = \Faker\Factory::create();
Then use like this
$sub_g->name = $faker->name();
$sub_g->country = $faker->country();
$sub_g->state = $faker->state;
Thank me later.

Laravel - Seeding Relationships

In Laravel, database seeding is generally accomplished through Model factories. So you define a blueprint for your Model using Faker data, and say how many instances you need:
$factory->define(App\User::class, function (Faker\Generator $faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => bcrypt(str_random(10)),
'remember_token' => str_random(10),
];
});
$user = factory(App\User::class, 50)->create();
However, lets say your User model has a hasMany relationship with many other Models, like a Post model for example:
Post:
id
name
body
user_id
So in this situation, you want to seed your Posts table with actual users that were seeded in your Users table. This doesn't seem to be explicitly discussed, but I did find the following in the Laravel docs:
$users = factory(App\User::class, 3)
->create()
->each(function($u) {
$u->posts()->save(factory(App\Post::class)->make());
});
So in your User factory, you create X number of Posts for each User you create. However, in a large application where maybe 50 - 75 Models share relationships with the User Model, your User Seeder would essentially end up seeding the entire database with all it's relationships.
My question is: Is this the best way to handle this? The only other thing I can think of is to Seed the Users first (without seeding any relations), and then pull random Users from the DB as needed while you are seeding other Models. However, in cases where they need to be unique, you'd have to keep track of which Users had been used. Also, it seems this would add a lot of extra query-bulk to the seeding process.
You can use saveMany as well. For example:
factory(User::class, 10)->create()->each(function ($user) {
$user->posts()->saveMany(factory(Posts::class, 5)->make());
});
You can do this using closures within the ModelFactory as discussed here.
This solution works cleanly and elegantly with seeders as well.
$factory->define(App\User::class, function (Faker\Generator $faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => bcrypt(str_random(10)),
'remember_token' => str_random(10),
];
});
$factory->define(App\Post::class, function (Faker\Generator $faker) {
return [
'name' => $faker->name,
'body' => $faker->paragraph(1),
'user_id' => function() {
return factory(App\User::class)->create()->id;
},
];
});
For your seeder, use something simple like this:
//create 10 users
factory(User::class, 10)->create()->each(function ($user) {
//create 5 posts for each user
factory(Post::class, 5)->create(['user_id'=>$user->id]);
});
NOTE: This method does not create unneeded entries in the database, instead the passed attributes are assigned BEFORE the creation of associated records.
Personally I think one Seeder class to manage these relations is nicer then separated seeder classes, because you have all the logic in one place, so in one look you can see what is going on. (Anyone that knows a better approach: please share) :)
A solution might be: one DatabaseSeeder and private methods within the class to keep the 'run' method a bit cleaner. I have this example below, which has a User, Link, LinkUser (many-to-many) and a Note (many-to-one).
For the many-to-many relations I first create all the Links, and get the inserted ids. (since the ids are auto-inc I think the ids could be fetched easier (get max), but doesn't matter in this example). Then create the users, and attach some random links to each user (many-to-many). It also creates random notes for each user (many-to-one example). It uses the 'factory' methods.
If you replace the 'Link' for your 'Post' this should work. (You can remove the 'Note' section then...)
(There is also a method to make sure you have 1 valid user with your own login credentials.)
<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* #return void
*/
public function run()
{
// Create random links
factory(App\Link::class, 100)->create();
// Fetch the link ids
$link_ids = App\Link::all('id')->pluck('id')->toArray();
// Create random users
factory(App\User::class, 50)->create()->each(function ($user) use ($link_ids) {
// Example: Many-to-many relations
$this->attachRandomLinksToUser($user->id, $link_ids);
// Example: Many-to-one relations
$this->createNotesForUserId( $user->id );
});
// Make sure you have a user to login with (your own email, name and password)
$this->updateCredentialsForTestLogin('john#doe.com', 'John Doe', 'my-password');
}
/**
* #param $user_id
* #param $link_ids
* #return void
*/
private function attachRandomLinksToUser($user_id, $link_ids)
{
$amount = random_int( 0, count($link_ids) ); // The amount of links for this user
echo "Attach " . $amount . " link(s) to user " . $user_id . "\n";
if($amount > 0) {
$keys = (array)array_rand($link_ids, $amount); // Random links
foreach($keys as $key) {
DB::table('link_user')->insert([
'link_id' => $link_ids[$key],
'user_id' => $user_id,
]);
}
}
}
/**
* #param $user_id
* #return void
*/
private function createNotesForUserId($user_id)
{
$amount = random_int(10, 50);
factory(App\Note::class, $amount)->create([
'user_id' => $user_id
]);
}
/**
* #param $email
* #param $name
* #param $password
* #return void
*/
private function updateCredentialsForTestLogin($email, $name, $password)
{
$user = App\User::where('email', $email)->first();
if(!$user) {
$user = App\User::find(1);
}
$user->name = $name;
$user->email = $email;
$user->password = bcrypt($password); // Or whatever you use for password encryption
$user->save();
}
}
$factory->define(App\User::class, function (Faker\Generator $faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => bcrypt(str_random(10)),
'remember_token' => str_random(10),
];
});
$factory->define(App\Post::class, function (Faker\Generator $faker) {
return [
'name' => $faker->name,
'body' => $faker->paragraph(1),
'user_id' => factory(App\User::class)->create()->id,
];
});
So now if you do this factory(App\Post::class, 4)->create() it will create 4 different posts and in the process also create 4 different users.
If you want the same user for all the posts what I usually do is:
$user = factory(App\User::class)->create();
$posts = factory(App\Posts::class, 40)->create(['user_id' => $user->id]);
I want to share the approach i've taken for insert many posts to many users:`
factory(App\User::class, 50)->create()
->each(
function ($u) {
factory(App\Post::class, 10)->create()
->each(
function($p) use (&$u) {
$u->posts()->save($p)->make();
}
);
}
);
`
This workaround worked for me after being all day long looking for a way to seed the relationship
this worked for me in laravel v8
for ($i=0; $i<=2; $i++) {
$user = \App\Models\User::factory(1)->create()->first();
$product = \App\Models\Product::factory(1)->create(['user_id' => $user->id])->first();
}
I use a custom made relateOrCreate function that finds a random entry of that model in the database. If none exist, it creates a new one:
function relateOrCreate($class) {
$instances = $class::all();
$instance;
if (count($instances) > 0) {
$randomIndex = rand(0, (count($instances) - 1));
$instance = $instances[$randomIndex];
}
else {
$instance = $class::factory()->create();
}
return $instance;
}
Then I use it like so:
$relatedUser = relateOrCreate(User::class);
return [
'user_id' => $relatedUser->id,
// ...
];

RBAC Yii2 doesn't work with default roles

I'm following the Definitive Guide to Yii 2.0. In my application I have two roles: the admin, who can do everything and the viewer, who can do some actions that unregistered users can't do. I'm trying to use default roles functionality of Yii 2 RBAC, but it seems doesn't work. The user table in my database has a column named "role": for admin it's value set to 1 and for viewers = 2.
What I did:
/app/rbac/UserGroupRule.php
namespace app\rbac;
use Yii;
use yii\rbac\Rule;
class UserGroupRule extends Rule {
public $name = 'userGroup';
public function execute($user, $item, $params) {
if (!Yii::$app->user->isGuest) {
$group = Yii::$app->user->identity->role;
if ($item->name === 'admin') {
return $group == 1;
} elseif ($item->name === 'viewer') {
return $group == 1 || $group == 2;
}
}
return false;
}
}
$auth = Yii::$app->authManager;
$rule = new \app\rbac\UserGroupRule;
$auth->add($rule);
$author = $auth->createRole('viewer');
$author->ruleName = $rule->name;
$auth->add($viewer);
$admin = $auth->createRole('admin');
$admin->ruleName = $rule->name;
$auth->add($admin);
$auth->addChild($admin, $viewer);
in my controller:
public function behaviors() {
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['admin'],
'rules' => [
[
'allow' => true,
'actions' => ['admin'],
'roles' => ['admin'],
],
],
],
];
}
When I try to access "admin" action, it says Forbidden #403, even when I'm an admin. How to make it work?
The user table in my database has a column named "role": for admin it's value set to 1 and for viewers = 2
That's not how it works unfortunately.
The rights/roles a user has are (by default) done via the auth_assignment-table.
Just add an entry in it:
INSERT INTO `auth_assignment` VALUES ("admin", <user-id>, NOW());
(be sure to change the user ID into whatever user you want to make admin.
That should solve your issue.
Edit (as I misread some of your question):
As per this link you can indeed define default roles, but you have to make sure to also reconfigure your authManager-component in the configuration file to include the default roles:
'components' => [
'authManager' => [
// ...
'defaultRoles' => ['admin', 'viewer'],
],
],
This list of roles indicate the permissions that always should be checked for every user, no matter if they have an entry in the auth_assignment-table or not.
I was facing the same issue with op. Finally made it work after tinkering with xdebug for a while.
I feel the official documentation on default roles is missing a couple important points, I will summarize them below with some of my personal experiences. The project structure is based on Yii 2.0 Advanced Project Template
Database
user table contains id and group. Where group is type int, 1 for admin and 2 for author
Rules setup
Code simplified for clarity.
The rule class, where you put the actual rule logic.
yii/console/controller/UserGroupRule.php
namespace app\rbac;
use Yii;
use yii\rbac\Rule;
/**
* Checks if user group matches
*/
class UserGroupRule extends Rule
{
public $name = 'userGroup';
public function execute($user, $item, $params)
{
if (!Yii::$app->user->isGuest) {
$group = Yii::$app->user->identity->group;
if ($item->name === 'admin') {
return $group == 1;
} elseif ($item->name === 'author') {
return $group == 1 || $group == 2;
}
}
return false;
}
}
Now defining the roles..
yii/console/controller/RbacController.php
namespace console\controllers;
use Yii;
use yii\console\Controller;
class RbacController extends Controller
{
public function actionInit()
{
$auth = Yii::$app->authManager;
$rule = new \app\rbac\UserGroupRule;
$auth->add($rule);
$admin = $auth->createRole('admin');
$admin->ruleName = $rule->name;
$auth->add($admin);
// define 'author' here...
}
}
After you have this file ready, you should be able to run ./yii rbac/init to generate the rule files:
console/rbac/items.php
console/rbac/rules.php
Important: You need to place the generated files under your desired application folder, this is crucial. Other wise Yii 2.0 will not be able to pick up the rules. For example: yii/backend/rbac/
Controller and config setup
This is mostly identical to the documentation
yii/commom/config/main.php
Add the following to the return array:
'authManager' => [
'class' => 'yii\rbac\PhpManager',
'defaultRoles' => ['admin', 'author'], // your define roles
],
Now the fun part, under the controller class you would like to apply the rules
yii/backend/controllers/SiteController.php
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'allow' => true,
'actions' => [], // applies to all actions
'roles' => ['admin'], // your defined roles
],
],
],
Up to this point, the rules should be working. Under your controller class, double check Yii::$app->getAuthManager() see if it contains your defined roles. If not, it means Yii did not pick up the rules correctly, please check previous steps again.

Categories