fix: duplicate email/phone shows notification instead of crashing
Two-layer protection on MasterResource and UserResource: 1. Form validation via ->rules() with Rule::unique() scoped to company_id (prevents DB hit in normal flow, shows inline field error) 2. handleRecordCreation/handleRecordUpdate catches UniqueConstraintViolationException as fallback — shows a Filament danger notification and halts without showing an error page Also adds phone uniqueness validation (scoped per tenant). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ use Filament\Tables;
|
|||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Validation\Rules\Unique;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tehnicieni — vedere filtrată peste users (role=mechanic).
|
* Tehnicieni — vedere filtrată peste users (role=mechanic).
|
||||||
@@ -62,16 +62,22 @@ class MasterResource extends Resource
|
|||||||
->columns(2)
|
->columns(2)
|
||||||
->schema([
|
->schema([
|
||||||
Forms\Components\TextInput::make('name')->label(__('Nume'))->required()->maxLength(120),
|
Forms\Components\TextInput::make('name')->label(__('Nume'))->required()->maxLength(120),
|
||||||
Forms\Components\TextInput::make('phone')->label(__('Telefon'))->tel()->maxLength(40),
|
Forms\Components\TextInput::make('phone')
|
||||||
|
->label(__('Telefon'))
|
||||||
|
->tel()
|
||||||
|
->maxLength(40)
|
||||||
|
->rules(fn ($record) => $record
|
||||||
|
? [Rule::unique('users', 'phone')->where('company_id', auth()->user()?->company_id)->ignore($record->id)]
|
||||||
|
: [Rule::unique('users', 'phone')->where('company_id', auth()->user()?->company_id)]
|
||||||
|
)
|
||||||
|
->validationMessages(['unique' => __('Acest număr de telefon este deja folosit de un alt utilizator.')]),
|
||||||
Forms\Components\TextInput::make('email')
|
Forms\Components\TextInput::make('email')
|
||||||
->label(__('Email'))
|
->label(__('Email'))
|
||||||
->email()
|
->email()
|
||||||
->maxLength(120)
|
->maxLength(120)
|
||||||
->unique(
|
->rules(fn ($record) => $record
|
||||||
table: 'users',
|
? [Rule::unique('users', 'email')->where('company_id', auth()->user()?->company_id)->ignore($record->id)]
|
||||||
column: 'email',
|
: [Rule::unique('users', 'email')->where('company_id', auth()->user()?->company_id)]
|
||||||
ignoreRecord: true,
|
|
||||||
modifyRuleUsing: fn (Unique $rule) => $rule->where('company_id', auth()->user()?->company_id),
|
|
||||||
)
|
)
|
||||||
->validationMessages(['unique' => __('Acest email este deja folosit de un alt utilizator.')]),
|
->validationMessages(['unique' => __('Acest email este deja folosit de un alt utilizator.')]),
|
||||||
Forms\Components\Select::make('status')
|
Forms\Components\Select::make('status')
|
||||||
|
|||||||
@@ -4,11 +4,28 @@ namespace App\Filament\Tenant\Resources\MasterResource\Pages;
|
|||||||
|
|
||||||
use App\Filament\Tenant\Resources\MasterResource;
|
use App\Filament\Tenant\Resources\MasterResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
use Filament\Support\Exceptions\Halt;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\UniqueConstraintViolationException;
|
||||||
|
|
||||||
class CreateMaster extends CreateRecord
|
class CreateMaster extends CreateRecord
|
||||||
{
|
{
|
||||||
protected static string $resource = MasterResource::class;
|
protected static string $resource = MasterResource::class;
|
||||||
|
|
||||||
|
protected function handleRecordCreation(array $data): Model
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return parent::handleRecordCreation($data);
|
||||||
|
} catch (UniqueConstraintViolationException $e) {
|
||||||
|
$msg = str_contains($e->getMessage(), 'email')
|
||||||
|
? __('Acest email este deja folosit de un alt utilizator.')
|
||||||
|
: __('Acest număr de telefon este deja folosit de un alt utilizator.');
|
||||||
|
|
||||||
|
Notification::make()->danger()->title($msg)->send();
|
||||||
|
|
||||||
|
throw new Halt();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,30 @@ namespace App\Filament\Tenant\Resources\MasterResource\Pages;
|
|||||||
|
|
||||||
use App\Filament\Tenant\Resources\MasterResource;
|
use App\Filament\Tenant\Resources\MasterResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
use Filament\Support\Exceptions\Halt;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\UniqueConstraintViolationException;
|
||||||
|
|
||||||
class EditMaster extends EditRecord
|
class EditMaster extends EditRecord
|
||||||
{
|
{
|
||||||
protected static string $resource = MasterResource::class;
|
protected static string $resource = MasterResource::class;
|
||||||
|
|
||||||
protected function getHeaderActions(): array { return [Actions\DeleteAction::make()]; }
|
protected function getHeaderActions(): array { return [Actions\DeleteAction::make()]; }
|
||||||
|
|
||||||
|
protected function handleRecordUpdate(Model $record, array $data): Model
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return parent::handleRecordUpdate($record, $data);
|
||||||
|
} catch (UniqueConstraintViolationException $e) {
|
||||||
|
$msg = str_contains($e->getMessage(), 'email')
|
||||||
|
? __('Acest email este deja folosit de un alt utilizator.')
|
||||||
|
: __('Acest număr de telefon este deja folosit de un alt utilizator.');
|
||||||
|
|
||||||
|
Notification::make()->danger()->title($msg)->send();
|
||||||
|
|
||||||
|
throw new Halt();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use Filament\Schemas;
|
|||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Validation\Rules\Unique;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class UserResource extends Resource
|
class UserResource extends Resource
|
||||||
{
|
{
|
||||||
@@ -73,14 +73,19 @@ class UserResource extends Resource
|
|||||||
->email()
|
->email()
|
||||||
->required()
|
->required()
|
||||||
->maxLength(120)
|
->maxLength(120)
|
||||||
->unique(
|
->rules(fn ($record) => $record
|
||||||
table: 'users',
|
? [Rule::unique('users', 'email')->where('company_id', auth()->user()?->company_id)->ignore($record->id)]
|
||||||
column: 'email',
|
: [Rule::unique('users', 'email')->where('company_id', auth()->user()?->company_id)]
|
||||||
ignoreRecord: true,
|
|
||||||
modifyRuleUsing: fn (Unique $rule) => $rule->where('company_id', auth()->user()?->company_id),
|
|
||||||
)
|
)
|
||||||
->validationMessages(['unique' => __('Acest email este deja folosit de un alt utilizator.')]),
|
->validationMessages(['unique' => __('Acest email este deja folosit de un alt utilizator.')]),
|
||||||
Forms\Components\TextInput::make('phone')->tel()->maxLength(40),
|
Forms\Components\TextInput::make('phone')
|
||||||
|
->tel()
|
||||||
|
->maxLength(40)
|
||||||
|
->rules(fn ($record) => $record
|
||||||
|
? [Rule::unique('users', 'phone')->where('company_id', auth()->user()?->company_id)->ignore($record->id)]
|
||||||
|
: [Rule::unique('users', 'phone')->where('company_id', auth()->user()?->company_id)]
|
||||||
|
)
|
||||||
|
->validationMessages(['unique' => __('Acest număr de telefon este deja folosit de un alt utilizator.')]),
|
||||||
Forms\Components\Select::make('locale')
|
Forms\Components\Select::make('locale')
|
||||||
->options(['ro' => __('Română'), 'ru' => 'Русский', 'en' => 'English'])
|
->options(['ro' => __('Română'), 'ru' => 'Русский', 'en' => 'English'])
|
||||||
->default('ro'),
|
->default('ro'),
|
||||||
|
|||||||
@@ -3,7 +3,11 @@
|
|||||||
namespace App\Filament\Tenant\Resources\UserResource\Pages;
|
namespace App\Filament\Tenant\Resources\UserResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Tenant\Resources\UserResource;
|
use App\Filament\Tenant\Resources\UserResource;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
use Filament\Support\Exceptions\Halt;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\UniqueConstraintViolationException;
|
||||||
|
|
||||||
class CreateUser extends CreateRecord
|
class CreateUser extends CreateRecord
|
||||||
{
|
{
|
||||||
@@ -12,6 +16,19 @@ class CreateUser extends CreateRecord
|
|||||||
/** @var array<string> */
|
/** @var array<string> */
|
||||||
protected array $_rolesPicked = [];
|
protected array $_rolesPicked = [];
|
||||||
|
|
||||||
|
protected function handleRecordCreation(array $data): Model
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return parent::handleRecordCreation($data);
|
||||||
|
} catch (UniqueConstraintViolationException $e) {
|
||||||
|
$msg = str_contains($e->getMessage(), 'email')
|
||||||
|
? __('Acest email este deja folosit de un alt utilizator.')
|
||||||
|
: __('Acest număr de telefon este deja folosit de un alt utilizator.');
|
||||||
|
Notification::make()->danger()->title($msg)->send();
|
||||||
|
throw new Halt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected function mutateFormDataBeforeCreate(array $data): array
|
protected function mutateFormDataBeforeCreate(array $data): array
|
||||||
{
|
{
|
||||||
$this->_rolesPicked = $data['roles_picked'] ?? [];
|
$this->_rolesPicked = $data['roles_picked'] ?? [];
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ namespace App\Filament\Tenant\Resources\UserResource\Pages;
|
|||||||
|
|
||||||
use App\Filament\Tenant\Resources\UserResource;
|
use App\Filament\Tenant\Resources\UserResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
use Filament\Support\Exceptions\Halt;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\UniqueConstraintViolationException;
|
||||||
|
|
||||||
class EditUser extends EditRecord
|
class EditUser extends EditRecord
|
||||||
{
|
{
|
||||||
@@ -18,6 +22,19 @@ class EditUser extends EditRecord
|
|||||||
/** @var array<string> */
|
/** @var array<string> */
|
||||||
protected array $_rolesPicked = [];
|
protected array $_rolesPicked = [];
|
||||||
|
|
||||||
|
protected function handleRecordUpdate(Model $record, array $data): Model
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return parent::handleRecordUpdate($record, $data);
|
||||||
|
} catch (UniqueConstraintViolationException $e) {
|
||||||
|
$msg = str_contains($e->getMessage(), 'email')
|
||||||
|
? __('Acest email este deja folosit de un alt utilizator.')
|
||||||
|
: __('Acest număr de telefon este deja folosit de un alt utilizator.');
|
||||||
|
Notification::make()->danger()->title($msg)->send();
|
||||||
|
throw new Halt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected function mutateFormDataBeforeSave(array $data): array
|
protected function mutateFormDataBeforeSave(array $data): array
|
||||||
{
|
{
|
||||||
$this->_rolesPicked = $data['roles_picked'] ?? [];
|
$this->_rolesPicked = $data['roles_picked'] ?? [];
|
||||||
|
|||||||
Reference in New Issue
Block a user