feat(auth): complete registration anti-spam and quota hardening
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
<?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 {
|
||||
if (! Schema::hasColumn('users', 'last_verification_sent_at')) {
|
||||
$table->timestamp('last_verification_sent_at')->nullable()->after('email_verified_at');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('users', 'verification_send_count_24h')) {
|
||||
$table->unsignedInteger('verification_send_count_24h')->default(0)->after('last_verification_sent_at');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('users', 'verification_send_window_started_at')) {
|
||||
$table->timestamp('verification_send_window_started_at')->nullable()->after('verification_send_count_24h');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
if (Schema::hasColumn('users', 'verification_send_window_started_at')) {
|
||||
$table->dropColumn('verification_send_window_started_at');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('users', 'verification_send_count_24h')) {
|
||||
$table->dropColumn('verification_send_count_24h');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('users', 'last_verification_sent_at')) {
|
||||
$table->dropColumn('last_verification_sent_at');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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
|
||||
{
|
||||
if (Schema::hasTable('email_send_events')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('email_send_events', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('type', 64);
|
||||
$table->string('email');
|
||||
$table->string('ip', 45)->nullable();
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('status', 32);
|
||||
$table->string('reason', 64)->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->index('email');
|
||||
$table->index('ip');
|
||||
$table->index(['type', 'status']);
|
||||
$table->index('created_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('email_send_events');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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
|
||||
{
|
||||
if (Schema::hasTable('system_email_quota')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('system_email_quota', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('period', 7)->unique();
|
||||
$table->unsignedInteger('sent_count')->default(0);
|
||||
$table->unsignedInteger('limit_count');
|
||||
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('system_email_quota');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('user_verification_tokens')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip rename on SQLite (in-memory tests) — SQLite doesn't support CHANGE syntax.
|
||||
try {
|
||||
$driver = DB::connection()->getPdo()->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
} catch (\Throwable $e) {
|
||||
$driver = null;
|
||||
}
|
||||
|
||||
if ($driver === 'sqlite') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use raw statement to avoid requiring doctrine/dbal for simple rename
|
||||
// Adjust column definition to match original migration (VARCHAR(128) NOT NULL)
|
||||
DB::statement("ALTER TABLE `user_verification_tokens` CHANGE `token` `token_hash` VARCHAR(128) NOT NULL");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('user_verification_tokens')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$driver = DB::connection()->getPdo()->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
} catch (\Throwable $e) {
|
||||
$driver = null;
|
||||
}
|
||||
|
||||
if ($driver === 'sqlite') {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::statement("ALTER TABLE `user_verification_tokens` CHANGE `token_hash` `token` VARCHAR(128) NOT NULL");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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
|
||||
{
|
||||
if (Schema::hasTable('user_verification_tokens')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('user_verification_tokens', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('token_hash', 128)->unique();
|
||||
$table->timestamp('expires_at');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['user_id', 'expires_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Intentionally no-op to avoid dropping an existing tokens table
|
||||
// that may have been created by earlier migrations.
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user