feat(auth): BannedIp model + migration

feat/v1-foundation
boban 2026-06-20 17:26:28 +02:00
parent 9756f9d463
commit 1f7b06c576
3 changed files with 66 additions and 0 deletions

19
app/Models/BannedIp.php Normal file
View File

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class BannedIp extends Model
{
protected $guarded = [];
protected $casts = ['banned_until' => 'datetime'];
/** Non-expired bans only. */
public function scopeActive(Builder $query): Builder
{
return $query->where('banned_until', '>', now());
}
}

View File

@ -0,0 +1,25 @@
<?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::create('banned_ips', function (Blueprint $table) {
$table->id();
$table->string('ip', 45)->unique();
$table->timestamp('banned_until');
$table->string('reason')->nullable();
$table->unsignedInteger('attempts')->default(0);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('banned_ips');
}
};

View File

@ -0,0 +1,22 @@
<?php
namespace Tests\Feature;
use App\Models\BannedIp;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class BruteforceBannedIpTest extends TestCase
{
use RefreshDatabase;
public function test_active_scope_excludes_expired_bans(): void
{
BannedIp::create(['ip' => '1.2.3.4', 'banned_until' => now()->addHour(), 'attempts' => 10]);
BannedIp::create(['ip' => '5.6.7.8', 'banned_until' => now()->subHour(), 'attempts' => 10]);
$active = BannedIp::active()->pluck('ip')->all();
$this->assertSame(['1.2.3.4'], $active);
}
}