From a7439dd6a00fcae1d8d3045dbec45bc827a7c6da Mon Sep 17 00:00:00 2001 From: Morris Jencen Chavez Date: Wed, 9 Sep 2026 19:19:45 +0000 Subject: [PATCH 1/2] fix: enforce email verification via MustVerifyEmail on User model The 'verified' route middleware guarding /dashboard, /endpoints, /events, and /deliveries has been structurally dead code: User never implemented Illuminate\Contracts\Auth\MustVerifyEmail, and Fortify's emailVerification feature was disabled, so the middleware silently let every request through regardless of verification status. The rest of the app already assumed this feature was live (a full VerifyEmail Vue page, a resend-link control in profile settings, and an EmailVerificationTest suite that skipped every case), so this wires it up rather than removing the middleware: - Implement MustVerifyEmail on App\Models\User. - Enable Features::emailVerification() in config/fortify.php. - Add EmailVerificationEnforcementTest covering that an unverified user is redirected away from the dashboard and a verified user can still reach it. Fixes #171 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LmKzHHTMtJob3d8jvRe6bz --- app/Models/User.php | 4 +-- config/fortify.php | 2 +- .../EmailVerificationEnforcementTest.php | 30 +++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 tests/Feature/EmailVerificationEnforcementTest.php diff --git a/app/Models/User.php b/app/Models/User.php index 70519e4..c728b9b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,8 +2,8 @@ namespace App\Models; -// use Illuminate\Contracts\Auth\MustVerifyEmail; use Database\Factories\UserFactory; +use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -14,7 +14,7 @@ use Laravel\Jetstream\HasTeams; use Laravel\Sanctum\HasApiTokens; -class User extends Authenticatable +class User extends Authenticatable implements MustVerifyEmail { use HasApiTokens; diff --git a/config/fortify.php b/config/fortify.php index 726d83b..0551d1d 100644 --- a/config/fortify.php +++ b/config/fortify.php @@ -146,7 +146,7 @@ 'features' => [ Features::registration(), Features::resetPasswords(), - // Features::emailVerification(), + Features::emailVerification(), Features::updateProfileInformation(), Features::updatePasswords(), Features::twoFactorAuthentication([ diff --git a/tests/Feature/EmailVerificationEnforcementTest.php b/tests/Feature/EmailVerificationEnforcementTest.php new file mode 100644 index 0000000..fd3ba5c --- /dev/null +++ b/tests/Feature/EmailVerificationEnforcementTest.php @@ -0,0 +1,30 @@ +unverified()->create(); + + $response = $this->actingAs($user)->get('/dashboard'); + + $response->assertRedirect(route('verification.notice')); + } + + public function test_verified_user_can_access_the_dashboard(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get('/dashboard'); + + $response->assertOk(); + } +} From 5224a4be256467b2c8442d1fb979c030e5a29361 Mon Sep 17 00:00:00 2001 From: Morris Jencen Chavez Date: Wed, 9 Sep 2026 19:27:25 +0000 Subject: [PATCH 2/2] fix: make event name generation collision-safe in seeder and factory The full test suite intermittently failed with a UniqueConstraintViolationException on events(user_id, name) once email verification enforcement added more factory-created events per test run, surfacing a latent bug: - WebhookSeeder generated names as `$eventType.'_'.time().'_'.rand(1000,9999)`, which can collide when multiple events of the same type are seeded within the same second. - EventFactory::definition() returned a name straight from a fixed pool with no uniqueness guarantee, so any test creating more than one event per user via the factory without an explicit name override could collide. Replace the seeder's timestamp+rand suffix with Str::ulid(), and append a Str::random(6) suffix in the factory default. Fixes #100 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LmKzHHTMtJob3d8jvRe6bz --- database/factories/EventFactory.php | 6 +++++- database/seeders/WebhookSeeder.php | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/database/factories/EventFactory.php b/database/factories/EventFactory.php index a529d95..8d8db4d 100644 --- a/database/factories/EventFactory.php +++ b/database/factories/EventFactory.php @@ -4,6 +4,7 @@ use App\Models\Event; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Str; /** * @extends Factory @@ -54,7 +55,10 @@ public function definition(): array $event = $this->faker->randomElement($eventTypes); return [ - 'name' => $event['name'], + // Append a random suffix so repeated factory calls for the same + // user can't collide against the (user_id, name) unique + // constraint (see #100). + 'name' => $event['name'].'_'.Str::random(6), 'description' => $event['description'], ]; } diff --git a/database/seeders/WebhookSeeder.php b/database/seeders/WebhookSeeder.php index 0b65bbd..a01bd0b 100644 --- a/database/seeders/WebhookSeeder.php +++ b/database/seeders/WebhookSeeder.php @@ -7,6 +7,7 @@ use App\Models\Event; use App\Models\User; use Illuminate\Database\Seeder; +use Illuminate\Support\Str; class WebhookSeeder extends Seeder { @@ -98,7 +99,7 @@ public function run(): void $event = Event::factory() ->for($user) ->create([ - 'name' => $eventType.'_'.time().'_'.rand(1000, 9999), + 'name' => $eventType.'_'.Str::ulid(), 'event_type' => $eventType, 'description' => 'Triggered when '.str_replace('.', ' ', $eventType).' occurs', ]); @@ -141,7 +142,7 @@ public function run(): void $event = Event::factory() ->for($user) ->create([ - 'name' => $eventType.'_recent_'.time().'_'.rand(1000, 9999), + 'name' => $eventType.'_recent_'.Str::ulid(), 'event_type' => $eventType, 'description' => 'Recent '.str_replace('.', ' ', $eventType).' event', 'created_at' => $createdAt,