Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

pc_magas's avatar

How I can have per User Repository Password Broker?

I am developing a custom CRM (cause I was paid to do so, therefore no objections) and I am in a case where I have 2 user tables:

  • users: Where is the default users table provided by laravel ans users the default user repository
  • customers: Where each entry may or may not have credentials.

For the latter I made a Custom user Provider:

class CustomerProvider extends EloquentUserProvider
{
    public function retrieveByCredentials(array $credentials)
    {
        if (empty($credentials) ||
            (count($credentials) === 1 && array_key_exists('password', $credentials))) {
            return null;
        }
        // Retrieve the customer based on the email and user_id
        $model = $this->createModel()->newQuery()
            ->where('email', $credentials['email'])
            ->where('business_id', $credentials['business_id'])
            ->first();

        return $model;
    }

    public function validateCredentials(UserContract $customer, array $credentials)
    {
        if(empty($credentials['business_id']) || (int)$customer->business_id !==(int)$credentials['business_id']){
           return false;
        }

        if(empty($customer->email) ||
            empty($credentials['email']) ||
            trim($customer->email) !== trim($credentials['email'])
        ){
            return false;
        }

        if(empty($customer->password) ||
            empty($credentials['password']) ||
            !Hash::check($credentials['password'],$customer->password)
        ){
            return false;
        }

        return true;
    }
}

Where my customer is created using this migration:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::dropIfExists('customer');

        Schema::create('customer', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->string('fullname');

            $table->string('email')->nullable();
            $table->string('password')->nullable();

            $table->bigInteger('business_id')->unsigned();
            $table->foreign('business_id','customer_business_id_fk')
                ->references('id')
                ->on('business')
                ->onUpdate('cascade')
                ->onDelete('cascade');
            
            $table->unique(['email', 'business_id']);

        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('customer');
    }
};

And has this Model:

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;

/**
 * A bassic Customer.
 *
 * @property int $id
 * @property string $fullname
 * @property string $email
 * @property string $phone
 *
 * @property-read string $profile_url
 * @property-read $files
 */
class Customer extends Authenticatable
{
    use HasFactory;

    protected $table='customer';

    protected $fillable = [
        'fullname',
        'business_id',
        'created_at',
        'email',
        'password'
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];


    public function setEmailAttribute($email)
    {
        $email = trim($email);
        if(empty($email)){
            $this->attributes['email']=trim($email);
        }

        // Controller does the validation therefore I do the sanitization here
        $email = filter_var($email,FILTER_SANITIZE_EMAIL);
        $this->attributes['email']=trim($email);
    }

    public function setPasswordAttribute($password)
    {
        if(is_string($password)){
            $password=trim($password);
        }

        if(empty($password)){
            $this->attributes['password']=null;
        }

        $this->attributes['password']=$password;
    }
}

Also my auth.php is:

return [
    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],
    'guards' => [
        // Default guard do not remove.
        'web' => [
            'driver' => 'session',
            'provider' => 'login',
        ],
        'customer'=>[
            'driver'=>'session',
            'provider'=>'customer'
        ]
    ],
    'providers' => [
        'login' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
        'customer'=>[
            'driver' => 'customer',
            'model' => App\Models\Customer::class
        ],
    ],
    'passwords' => [
        'users' => [
            'provider' => 'login', // Use the 'login' provider for regular users
            'table' => 'password_reset_tokens', // Name of the database table
            'expire' => 60, // Expiration time for the reset token (in minutes)
            'throttle' => 60, // Throttle time (in seconds)
        ],
        'customers' => [
            'provider' => 'customer', // Use the 'customer' provider for customers
            'table' => 'customer_password_reset_tokens', // Name of the database table for customers
            'expire' => 60,
            'throttle' => 60,
        ],
    ],
    'password_timeout' => 10800,
];

The customer_password_reset_tokens is created using this migration:

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::dropIfExists('customer_password_reset_tokens');

        Schema::create('customer_password_reset_tokens', function (Blueprint $table) {
            $table->string('email');
            $table->bigInteger('business_id')->unsigned();

            $table->string('token');
            $table->timestamp('created_at')->nullable();

            $table->primary(['email','business_id']);

            $table->foreign('business_id')->references('business_id')
                ->on('customer')
                ->onDelete('cascade')
                ->onUpdate('cascade');

            $table->foreign('email')->references('email')
                ->on('customer')
                ->onDelete('cascade')
                ->onUpdate('cascade');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('customer_password_reset_tokens');
    }
};

But if I use the password broker in order to create a new password roken:


$status = Password::broker('customers')->sendResetLink(['email'=>$request->email,'business_id'=>$request->business_id]);

I get this error:

   Illuminate\Database\QueryException  SQLSTATE[HY000]: General error: 1364 Field 'business_id' doesn't have a default value (Connection: mysql, SQL: insert into `customer_password_reset_tokens` (`email`, `token`, `created_at`) values ([email protected], yAu2fYy8rGsNBr//hoSPMOHf.TNrSHg2Mw5ebABQQ.72P5/bBAJam, 2024-03-11 13:25:45)).

In my case identifying a customer only using email is not viable because according to spec emails can be duplicate but businesses not. Therefore I need both columns email and business_id in order to identify the user. But for users instead email is enough to identify them according to default migration:

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('users');
    }
};

Therefor how I can provide a custom Password proker class ONLY for customers where users cvan use the default one?

0 likes
0 replies

Please or to participate in this conversation.