fix(auth): scope email uniqueness to active users

This commit is contained in:
2026-09-03 09:59:21 -03:00
parent ddb8c3743f
commit 0baad641d5
7 changed files with 73 additions and 6 deletions

View File

@@ -19,7 +19,7 @@ use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
#[Fillable(['nombre_apellido', 'email', 'password', 'dni', 'telefono', 'google_id', 'rol_codigo', 'tenant_codigo'])] #[Fillable(['nombre_apellido', 'email', 'password', 'dni', 'telefono', 'google_id', 'rol_codigo', 'tenant_codigo'])]
#[Hidden(['password', 'remember_token'])] #[Hidden(['password', 'remember_token', 'active_email'])]
class User extends Authenticatable class User extends Authenticatable
{ {
/** @use HasFactory<UserFactory> */ /** @use HasFactory<UserFactory> */

View File

@@ -21,7 +21,13 @@ class RegisterUserRequest extends FormRequest
return [ return [
'tenant_codigo' => ['nullable', 'string', Rule::exists('tenants', 'codigo')], 'tenant_codigo' => ['nullable', 'string', Rule::exists('tenants', 'codigo')],
'nombre_apellido' => ['required', 'string', 'max:255'], 'nombre_apellido' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users', 'email')], 'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique('users', 'email')->whereNull('deleted_at'),
],
'password' => ['required', 'string', 'confirmed', Password::min(8)->mixedCase()->symbols()], 'password' => ['required', 'string', 'confirmed', Password::min(8)->mixedCase()->symbols()],
'dni' => ['nullable', 'string', 'max:255'], 'dni' => ['nullable', 'string', 'max:255'],
'telefono' => ['nullable', 'string', 'max:255'], 'telefono' => ['nullable', 'string', 'max:255'],

View File

@@ -4,6 +4,7 @@ namespace App\Domains\Auth\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
class UpdateProfileRequest extends FormRequest class UpdateProfileRequest extends FormRequest
{ {
@@ -19,11 +20,13 @@ class UpdateProfileRequest extends FormRequest
'email' => [ 'email' => [
'required', 'required',
'email', 'email',
Rule::unique('users', 'email')->ignore($this->user()->id), Rule::unique('users', 'email')
->whereNull('deleted_at')
->ignore($this->user()->id),
], ],
'dni' => ['nullable', 'string', 'regex:/^[0-9]{7,8}$/'], 'dni' => ['nullable', 'string', 'regex:/^[0-9]{7,8}$/'],
'telefono' => ['nullable', 'string', 'regex:/^\+?[0-9\s\-]+$/'], 'telefono' => ['nullable', 'string', 'regex:/^\+?[0-9\s\-]+$/'],
'password' => ['nullable', 'string', \Illuminate\Validation\Rules\Password::min(8)->mixedCase()->symbols()], 'password' => ['nullable', 'string', Password::min(8)->mixedCase()->symbols()],
]; ];
} }
} }

View File

@@ -23,7 +23,12 @@ class StoreStaffRequest extends FormRequest
return [ return [
'nombre_apellido' => ['required', 'string', 'max:255'], 'nombre_apellido' => ['required', 'string', 'max:255'],
'dni' => ['required', 'string', 'max:50'], 'dni' => ['required', 'string', 'max:50'],
'email' => ['required', 'email', 'max:255', 'unique:users,email'], 'email' => [
'required',
'email',
'max:255',
Rule::unique('users', 'email')->whereNull('deleted_at'),
],
'category_ids' => $categoryRules, 'category_ids' => $categoryRules,
'category_ids.*' => ['integer', 'distinct', Rule::exists('categorias', 'id')], 'category_ids.*' => ['integer', 'distinct', Rule::exists('categorias', 'id')],
]; ];

View File

@@ -28,7 +28,9 @@ class UpdateStaffRequest extends FormRequest
'required', 'required',
'email', 'email',
'max:255', 'max:255',
Rule::unique('users', 'email')->ignore($staffId), Rule::unique('users', 'email')
->whereNull('deleted_at')
->ignore($staffId),
], ],
'category_ids' => $categoryRules, 'category_ids' => $categoryRules,
'category_ids.*' => ['integer', 'distinct', Rule::exists('categorias', 'id')], 'category_ids.*' => ['integer', 'distinct', Rule::exists('categorias', 'id')],

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->dropUnique(['email']);
$table->string('active_email')
->nullable()
->storedAs('CASE WHEN `deleted_at` IS NULL THEN LOWER(`email`) ELSE NULL END');
$table->unique('active_email');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->dropUnique(['active_email']);
$table->dropColumn('active_email');
$table->unique('email');
});
}
};

View File

@@ -121,6 +121,11 @@ class StaffControllerTest extends TestCase
$this->deleteJson("/api/v1/adminapp/tenant/staff/{$staffId}")->assertNoContent(); $this->deleteJson("/api/v1/adminapp/tenant/staff/{$staffId}")->assertNoContent();
$this->assertSoftDeleted('users', ['id' => $staffId]); $this->assertSoftDeleted('users', ['id' => $staffId]);
$this->assertDatabaseHas('users', [
'id' => $staffId,
'email' => 'ada@example.test',
'active_email' => null,
]);
$this->assertDatabaseMissing('personal_access_tokens', ['id' => $accessTokenId]); $this->assertDatabaseMissing('personal_access_tokens', ['id' => $accessTokenId]);
$this->assertDatabaseHas('category_scanners', [ $this->assertDatabaseHas('category_scanners', [
'user_id' => $staffId, 'user_id' => $staffId,
@@ -133,6 +138,24 @@ class StaffControllerTest extends TestCase
->assertOk() ->assertOk()
->assertJsonCount(0, 'data'); ->assertJsonCount(0, 'data');
$replacementResponse = $this->postJson('/api/v1/adminapp/tenant/staff', [
'nombre_apellido' => 'Nueva Ada',
'dni' => '11223344',
'email' => 'ADA@example.test',
'category_ids' => [$firstCategory->id],
])->assertSuccessful()
->assertJsonPath('data.email', 'ada@example.test');
$replacementStaffId = $replacementResponse->json('data.id');
$this->assertNotSame($staffId, $replacementStaffId);
$this->assertSame('Ada Byron', $ticket->fresh()->scannerUser?->nombre_apellido);
$this->assertDatabaseHas('users', [
'id' => $replacementStaffId,
'email' => 'ada@example.test',
'active_email' => 'ada@example.test',
'deleted_at' => null,
]);
} }
public function test_admin_cannot_assign_another_tenants_category(): void public function test_admin_cannot_assign_another_tenants_category(): void