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?
Please or to participate in this conversation.