I have created a new site using Jetstream and Inertia. Currently the application will return a "The email has already been taken." message if a user tries to register with an existing email. Notwithstanding timing analysis, I would like to keep the existence of user accounts private. Is there a way to keep the unique constraint on email but display the same outward behavior if someone registers with an existing email? Ideally, I would like to not create a second user, but email the existing user suggesting they reset their password or ignore the email.
Here's what worked for me:
app/Exceptions/ExistingUserException.php
namespace App\Exceptions;
use Illuminate\Validation\ValidationException;
class ExistingUserException extends ValidationException
{
}
app/Actions/Fortify/CreateNewUser.php
, throwing the extended ValidationException
if the form is otherwise good Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255'],
'password' => $this->passwordRules(),
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['required', 'accepted'] : '',
])->validate();
$validator = Validator::make($input, [
'email' => ['unique:users'],
], ['email.unique'=>'']);
if ($validator->fails())
{
throw new ExistingUserException($validator);
}
app/Http/Middleware/CatchExistingUser.php
<?php
namespace App\Http\Middleware;
use App\Exceptions\ExistingUserException;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\URL;
class CatchExistingUser
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next, $redirectToRoute = null)
{
$response = $next($request);
if ($response->exception && $response->exception instanceof ExistingUserException)
{
return $request->expectsJson()
? abort(403, 'Your email address is not verified.')
: Redirect::guest(URL::route($redirectToRoute ?: 'verification.notice'));
}
return $response;
}
}
config/fortify.php
'middleware' => [CatchExistingUser::class, 'web'],
routes/web.php
use Illuminate\Http\Request;
use Laravel\Fortify\Contracts\VerifyEmailViewResponse;
...
Route::get('/email/verify', function (Request $request) {
$user = $request->user();
if ($user && $user->hasVerifiedEmail())
{
return redirect()->intended(config('fortify.home'));
}
return app(VerifyEmailViewResponse::class);
})
->name('verification.notice');
The custom exception is not ideal but it seems cleaner than testing the validator stored in the ValidatorException
and then removing one message if there's more than one error. I think this would be needed to allow the validation of other fields while not leaking email uniqueness.