user disabled_at field addedù

This commit is contained in:
Giuseppe Naponiello
2026-06-24 17:02:11 +02:00
parent 96b0dc76aa
commit f8206b5840
5 changed files with 120 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Laravel\Fortify\Fortify;
/**
* Login custom: oltre a verificare le credenziali, blocca gli utenti con
* `disabled_at` impostato (es. legacy `is_active=false`). I soft-deleted sono
* già esclusi dalla query di default (scope SoftDeletes sul model User), non
* serve un controllo esplicito.
*
* Registrata via `Fortify::authenticateUsing()`: viene consultata sia dal
* pre-check 2FA (`RedirectIfTwoFactorAuthenticatable`) sia dal fallback
* (`AttemptToAuthenticate`), quindi un solo punto di applicazione del gate.
*/
class AuthenticateUser
{
public function __invoke(Request $request): ?User
{
$user = User::where(Fortify::username(), $request->input(Fortify::username()))->first();
if (! $user || ! Hash::check((string) $request->input('password'), $user->password)) {
return null;
}
return $user->disabled_at === null ? $user : null;
}
}

View File

@@ -73,6 +73,7 @@ class User extends Authenticatable implements Auditable, MustVerifyEmail
'two_factor_confirmed_at' => 'datetime',
'is_system' => 'boolean',
'anonymized_at' => 'datetime',
'disabled_at' => 'datetime',
];
}

View File

@@ -2,6 +2,7 @@
namespace App\Providers;
use App\Actions\Fortify\AuthenticateUser;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
@@ -29,6 +30,7 @@ class FortifyServiceProvider extends ServiceProvider
*/
public function boot(): void
{
Fortify::authenticateUsing(new AuthenticateUser);
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);

View File

@@ -0,0 +1,32 @@
<?php
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::table('users', function (Blueprint $table) {
// Login disabilitato (es. legacy is_active=false): blocco reversibile
// dell'accesso, indipendente da deleted_at (cestino) e anonymized_at
// (GDPR) — un utente disabilitato resta a tutti gli effetti nelle
// liste/affiliazioni, solo non può autenticarsi.
$table->timestamp('disabled_at')->nullable()->after('anonymized_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('disabled_at');
});
}
};

View File

@@ -0,0 +1,53 @@
<?php
namespace Tests\Feature\Auth;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Auth;
use Tests\TestCase;
/**
* Verifica il gate di login custom (`App\Actions\Fortify\AuthenticateUser`),
* registrato via `Fortify::authenticateUsing()`.
*/
class LoginTest extends TestCase
{
use RefreshDatabase;
public function test_login_succeeds_with_valid_credentials(): void
{
$user = $this->operationalUser();
$this->postJson('/api/login', [
'email' => $user->email,
'password' => 'password',
])->assertOk()->assertJsonPath('two_factor', false);
$this->assertTrue(Auth::check());
$this->assertSame($user->id, Auth::id());
}
public function test_login_fails_with_wrong_password(): void
{
$user = $this->operationalUser();
$this->postJson('/api/login', [
'email' => $user->email,
'password' => 'wrong-password',
])->assertUnprocessable();
$this->assertFalse(Auth::check());
}
public function test_login_fails_for_disabled_user(): void
{
$user = $this->operationalUser(['disabled_at' => now()]);
$this->postJson('/api/login', [
'email' => $user->email,
'password' => 'password',
])->assertUnprocessable();
$this->assertFalse(Auth::check());
}
}