Add phone-verified user registration

Two-step flow: request a one-time code by email/phone (RegistrationVerification,
mailed via RegistrationCodeMail, rate-limited by the new api-otp limiter keyed
to the identifier), then verify the code and register with RegistrationController.
User gains a phone column/fillable.
This commit is contained in:
Nyan Lin Paing
2026-08-30 14:53:18 +07:00
parent b6934e1fb5
commit 914b7f97f3
14 changed files with 677 additions and 0 deletions
@@ -0,0 +1,57 @@
<?php
namespace Modules\Identity\Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Modules\Identity\Models\RegistrationVerification;
/**
* @extends Factory<RegistrationVerification>
*/
class RegistrationVerificationFactory extends Factory
{
protected $model = RegistrationVerification::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'identifier' => fake()->unique()->safeEmail(),
'type' => 'email',
'code' => Hash::make('123456'),
'attempts' => 0,
'verified_at' => null,
'verification_token' => null,
'consumed_at' => null,
'expires_at' => now()->addMinutes(10),
];
}
public function phone(): self
{
return $this->state(fn (array $attributes) => [
'identifier' => fake()->numerify('+959#########'),
'type' => 'phone',
]);
}
public function verified(): self
{
return $this->state(fn (array $attributes) => [
'verified_at' => now(),
'verification_token' => str()->random(64),
]);
}
public function expired(): self
{
return $this->state(fn (array $attributes) => [
'expires_at' => now()->subMinute(),
]);
}
}
@@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone')->nullable()->unique()->after('email');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('phone');
});
}
};
@@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('registration_verifications', function (Blueprint $table) {
$table->id();
$table->string('identifier')->unique();
$table->string('type');
$table->string('code');
$table->unsignedTinyInteger('attempts')->default(0);
$table->timestamp('verified_at')->nullable();
$table->string('verification_token')->nullable()->unique();
$table->timestamp('consumed_at')->nullable();
$table->timestamp('expires_at');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('registration_verifications');
}
};