feat: Enable email verification on sign up (#97)
## Summary - Enables `MustVerifyEmail` on the `User` model so new users must verify their email before accessing protected routes - Unverified users are redirected to `/email/verify` when attempting to access routes behind the `verified` middleware (subscribe, onboarding, dashboard) - A verification email is automatically sent on registration via Fortify ## Screenshot  ## Video https://github.com/whisper-money/whisper-money/raw/mail-verification/storage/videos/ac0945c527f014b9cd657f18c911f496.webm ## Test plan - [x] New test: registration sends a `VerifyEmail` notification - [x] New test: newly registered users have `email_verified_at` as null - [x] New test: unverified users are redirected to `verification.notice` from protected routes (subscribe, onboarding, dashboard) - [x] New test: verified users are not redirected to verification notice - [x] Existing email verification and notification tests still pass (31 auth tests, 52 settings tests) - [x] Browser walkthrough: registration → redirected to `/email/verify` page with "Resend verification email" button
This commit is contained in:
parent
1500e5cd91
commit
370d388d99
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use App\Enums\DripEmailType;
|
||||
use App\Notifications\VerifyEmailNotification;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -15,7 +16,7 @@ use Laravel\Cashier\Billable;
|
|||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Pennant\Concerns\HasFeatures;
|
||||
|
||||
class User extends Authenticatable
|
||||
class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use Billable, HasFactory, HasFeatures, HasUuids, Notifiable, TwoFactorAuthenticatable;
|
||||
|
|
@ -133,4 +134,9 @@ class User extends Authenticatable
|
|||
{
|
||||
return $this->email === config('app.demo.email');
|
||||
}
|
||||
|
||||
public function sendEmailVerificationNotification(): void
|
||||
{
|
||||
$this->notify(new VerifyEmailNotification);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class VerifyEmailNotification extends VerifyEmail
|
||||
{
|
||||
public function toMail($notifiable): MailMessage
|
||||
{
|
||||
$verificationUrl = $this->verificationUrl($notifiable);
|
||||
|
||||
return (new MailMessage)
|
||||
->from(config('mail.from.address', 'hello@example.com'), 'Victor')
|
||||
->subject('Verify Your Email - Whisper Money')
|
||||
->markdown('mail.verify-email', [
|
||||
'userName' => $notifiable->name,
|
||||
'verificationUrl' => $verificationUrl,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<x-mail::message>
|
||||
# Verify your email, {{ $userName }}!
|
||||
|
||||
Hi! I'm Victor, the founder of Whisper Money. Thanks for signing up — I just need you to verify your email address to get started.
|
||||
|
||||
Once verified, you'll be able to set up your encryption key and start tracking your finances with full privacy.
|
||||
|
||||
<x-mail::button :url="$verificationUrl">
|
||||
Verify Email Address
|
||||
</x-mail::button>
|
||||
|
||||
If you didn't create a Whisper Money account, you can safely ignore this email.
|
||||
|
||||
Best,<br>
|
||||
Víctor F,<br>
|
||||
Founder of Whisper Money
|
||||
|
||||
<x-mail::subcopy>
|
||||
If you're having trouble clicking the "Verify Email Address" button, copy and paste the URL below into your web browser: <span class="break-all">[{{ $verificationUrl }}]({{ $verificationUrl }})</span>
|
||||
</x-mail::subcopy>
|
||||
</x-mail::message>
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
|
|
@ -12,9 +12,7 @@ it('can register a new user', function () {
|
|||
->fill('password_confirmation', 'password123')
|
||||
->click('@register-user-button')
|
||||
->wait(2)
|
||||
->assertSee('Welcome to')
|
||||
->assertSee('Whisper Money')
|
||||
->assertPathIs('/onboarding')
|
||||
->assertPathIs('/email/verify')
|
||||
->assertNoJavascriptErrors();
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use App\Models\User;
|
|||
// Basic Redirect Tests
|
||||
// =============================================================================
|
||||
|
||||
it('redirects new registration to onboarding page', function () {
|
||||
it('redirects new registration to email verification', function () {
|
||||
$page = visit('/register');
|
||||
|
||||
$page->assertSee('Create an account')
|
||||
|
|
@ -18,7 +18,7 @@ it('redirects new registration to onboarding page', function () {
|
|||
->fill('password_confirmation', 'password123456')
|
||||
->click('@register-user-button')
|
||||
->wait(3)
|
||||
->assertPathIs('/onboarding')
|
||||
->assertPathIs('/email/verify')
|
||||
->assertNoJavascriptErrors();
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
|
|
@ -365,17 +365,15 @@ it('completes onboarding flow through account creation', function () {
|
|||
// Create a bank for the account creation step
|
||||
Bank::factory()->create(['name' => 'Chase Bank']);
|
||||
|
||||
$page = visit('/register');
|
||||
$user = User::factory()->create([
|
||||
'onboarded_at' => null,
|
||||
]);
|
||||
|
||||
// Step 1: Register
|
||||
$page->assertSee('Create an account')
|
||||
->fill('name', 'E2E Test User')
|
||||
->fill('email', 'e2e-onboarding@example.com')
|
||||
->fill('password', 'SecurePassword123!')
|
||||
->fill('password_confirmation', 'SecurePassword123!')
|
||||
->click('@register-user-button')
|
||||
->wait(3)
|
||||
->assertPathIs('/onboarding')
|
||||
$this->actingAs($user);
|
||||
|
||||
$page = visit('/onboarding');
|
||||
|
||||
$page->assertPathIs('/onboarding')
|
||||
->assertNoJavascriptErrors();
|
||||
|
||||
// Step 2: Welcome
|
||||
|
|
@ -428,7 +426,7 @@ it('completes onboarding flow through account creation', function () {
|
|||
->assertNoJavascriptErrors();
|
||||
|
||||
// Verify user's encryption was set up
|
||||
$user = User::where('email', 'e2e-onboarding@example.com')->first();
|
||||
$user->refresh();
|
||||
expect($user->encryption_salt)->not->toBeNull();
|
||||
|
||||
// Verify account was created
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ test('email verification screen can be rendered', function () {
|
|||
|
||||
$response = $this->actingAs($user)->get(route('verification.notice'));
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertSuccessful();
|
||||
});
|
||||
|
||||
test('email can be verified', function () {
|
||||
|
|
@ -90,3 +90,23 @@ test('already verified user visiting verification link is redirected without fir
|
|||
expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
|
||||
Event::assertNotDispatched(Verified::class);
|
||||
});
|
||||
|
||||
test('unverified user is redirected to verification notice from protected routes', function (string $route) {
|
||||
$user = User::factory()->unverified()->onboarded()->create();
|
||||
|
||||
$this->actingAs($user)->get(route($route))
|
||||
->assertRedirect(route('verification.notice'));
|
||||
})->with([
|
||||
'subscribe',
|
||||
'onboarding',
|
||||
'dashboard',
|
||||
]);
|
||||
|
||||
test('verified user is not redirected to verification notice', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get(route('subscribe'));
|
||||
|
||||
expect($response->headers->get('Location'))
|
||||
->not->toBe(route('verification.notice'));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Notifications\VerifyEmailNotification;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
test('registration screen can be rendered', function () {
|
||||
$response = $this->get(route('register'));
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertSuccessful();
|
||||
});
|
||||
|
||||
test('new users can register', function () {
|
||||
|
|
@ -21,3 +24,33 @@ test('new users can register', function () {
|
|||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(route('onboarding', absolute: false));
|
||||
});
|
||||
|
||||
test('new users receive a verification email on registration', function () {
|
||||
Notification::fake();
|
||||
|
||||
$this->post(route('register.store'), [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
|
||||
$user = User::where('email', 'test@example.com')->first();
|
||||
|
||||
Notification::assertSentTo($user, VerifyEmailNotification::class);
|
||||
});
|
||||
|
||||
test('new users are not verified after registration', function () {
|
||||
Queue::fake();
|
||||
|
||||
$this->post(route('register.store'), [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
|
||||
$user = User::where('email', 'test@example.com')->first();
|
||||
|
||||
expect($user->hasVerifiedEmail())->toBeFalse();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use App\Notifications\VerifyEmailNotification;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
test('sends verification notification', function () {
|
||||
|
|
@ -15,7 +15,7 @@ test('sends verification notification', function () {
|
|||
->post(route('verification.send'))
|
||||
->assertRedirect(route('home'));
|
||||
|
||||
Notification::assertSentTo($user, VerifyEmail::class);
|
||||
Notification::assertSentTo($user, VerifyEmailNotification::class);
|
||||
});
|
||||
|
||||
test('does not send verification notification if email is verified', function () {
|
||||
|
|
|
|||
Loading…
Reference in New Issue