I'm building an API using Laravel 10 with Sanctum for authentication, and I'm encountering an issue with email verification. After the user clicks the verification link, the email_verified_at field remains null.
This is my register component
import React from "react";
import { useNavigate } from "react-router-dom";
function Register() {
const [name, setName] = React.useState("");
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const navigate = useNavigate();
React.useEffect(() => {
if (localStorage.getItem("token")) {
navigate("/account");
window.location.reload();
}
}, []);
async function register() {
const item = { name, email, password };
let result = await fetch("http://localhost:8000/api/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(item),
});
const data = await result.json();
if (result.ok) {
alert("Please verify your email before logging in.");
navigate("/login");
} else {
alert(data.message || "Error registering.");
}
}
return (
<div className="py-16">
<div className="flex items-center flex-col justify-center gap-6">
<div className="flex items-center justify-center">
<h1>Register Form</h1>
</div>
<div className="flex items-center flex-col gap-5 justify-center">
<input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div>
<button onClick={register} className="border-2 border-black p-2">
Register
</button>
</div>
</div>
</div>
);
}
export default Register;
This is my auth controller
<?php
namespace App\Http\Controllers;
use Illuminate\Validation\ValidationException; //manually imported
use Illuminate\Support\Facades\Hash; //manually imported
use Illuminate\Http\Request;
use App\Models\User; //manualy imported
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Carbon\Carbon;
class AuthController extends Controller
{
//
public function verifyEmail(Request $request, $id, $hash)
{
$user = User::find($id);
if (!$user) {
return response()->json(['message' => 'User not found.'], 404);
}
if (!hash_equals(sha1($user->getEmailForVerification()), $hash)) {
return response()->json(['message' => 'Invalid verification link.'], 400);
}
if ($user->email_verified_at) {
return response()->json(['message' => 'Email already verified.']);
}
$user->email_verified_at = Carbon::now();
$user->save();
return response()->json(['message' => 'Email verified successfully.']);
}
public function resendEmailVerification(Request $request)
{
if ($request->user()->hasVerifiedEmail()) {
return response()->json(['message' => 'Email already verified.']);
}
$request->user()->sendEmailVerificationNotification();
return response()->json(['message' => 'Verification email sent.']);
}
public function register(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|unique:users',
'password' => 'required|string|min:6'
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password)
]);
$user->sendEmailVerificationNotification();
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json(['token' => $token, 'user' => $user], 201);
}
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required'
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return ["error" => "Invalid credentials"];
}
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json(['token' => $token, 'user' => $user]);
}
public function logout(Request $request)
{
$request->user()->tokens()->delete();
return response()->json(['message' => 'Logged out']);
}
public function user(Request $request)
{
return response()->json($request->user());
}
}
These are my api routes
<?php
use App\Http\Controllers\AuthController;
use App\Http\Controllers\DisplayController;
use App\Http\Controllers\OrderController;
use App\Http\Controllers\StripeController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Route::post("/login",[AuthController::class,"login"]);
Route::post("/register",[AuthController::class,"register"]);
Route::middleware('auth:sanctum')->post('/addOrder', [OrderController::class, 'addOrder']);
Route::middleware('auth:sanctum')->post('/pay', [StripeController::class, 'pay']);
Route::middleware('auth:sanctum')->get('/displayOrders', [DisplayController::class, 'displayOrders']);
Route::get('/email/verify/{id}/{hash}', [AuthController::class, 'verifyEmail'])
->middleware(['signed'])
->name('verification.verify');
And these are my web routes
<?php
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use App\Http\Controllers\AuthController;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('welcome');
});
Route::get('/login', function () {
return redirect('http://localhost:5173/login');
})->name('login');
Route::get('/email/verify/{id}/{hash}', [AuthController::class, 'verifyEmail'])
->middleware(['auth:sanctum', 'signed'])
->name('verification.verify');
I already made sure that the user model has the email verification thing enabled. Simply , after I receive the confirmation email on mailtrap and click the button I am getting redirected to the login page and then when checking the data base the "email_verified_at" entry is still empty... pls help
I'm building an API using Laravel 10 with Sanctum for authentication, and I'm encountering an issue with email verification. After the user clicks the verification link, the email_verified_at field remains null.
This is my register component
import React from "react";
import { useNavigate } from "react-router-dom";
function Register() {
const [name, setName] = React.useState("");
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const navigate = useNavigate();
React.useEffect(() => {
if (localStorage.getItem("token")) {
navigate("/account");
window.location.reload();
}
}, []);
async function register() {
const item = { name, email, password };
let result = await fetch("http://localhost:8000/api/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(item),
});
const data = await result.json();
if (result.ok) {
alert("Please verify your email before logging in.");
navigate("/login");
} else {
alert(data.message || "Error registering.");
}
}
return (
<div className="py-16">
<div className="flex items-center flex-col justify-center gap-6">
<div className="flex items-center justify-center">
<h1>Register Form</h1>
</div>
<div className="flex items-center flex-col gap-5 justify-center">
<input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div>
<button onClick={register} className="border-2 border-black p-2">
Register
</button>
</div>
</div>
</div>
);
}
export default Register;
This is my auth controller
<?php
namespace App\Http\Controllers;
use Illuminate\Validation\ValidationException; //manually imported
use Illuminate\Support\Facades\Hash; //manually imported
use Illuminate\Http\Request;
use App\Models\User; //manualy imported
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Carbon\Carbon;
class AuthController extends Controller
{
//
public function verifyEmail(Request $request, $id, $hash)
{
$user = User::find($id);
if (!$user) {
return response()->json(['message' => 'User not found.'], 404);
}
if (!hash_equals(sha1($user->getEmailForVerification()), $hash)) {
return response()->json(['message' => 'Invalid verification link.'], 400);
}
if ($user->email_verified_at) {
return response()->json(['message' => 'Email already verified.']);
}
$user->email_verified_at = Carbon::now();
$user->save();
return response()->json(['message' => 'Email verified successfully.']);
}
public function resendEmailVerification(Request $request)
{
if ($request->user()->hasVerifiedEmail()) {
return response()->json(['message' => 'Email already verified.']);
}
$request->user()->sendEmailVerificationNotification();
return response()->json(['message' => 'Verification email sent.']);
}
public function register(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|unique:users',
'password' => 'required|string|min:6'
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password)
]);
$user->sendEmailVerificationNotification();
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json(['token' => $token, 'user' => $user], 201);
}
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required'
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return ["error" => "Invalid credentials"];
}
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json(['token' => $token, 'user' => $user]);
}
public function logout(Request $request)
{
$request->user()->tokens()->delete();
return response()->json(['message' => 'Logged out']);
}
public function user(Request $request)
{
return response()->json($request->user());
}
}
These are my api routes
<?php
use App\Http\Controllers\AuthController;
use App\Http\Controllers\DisplayController;
use App\Http\Controllers\OrderController;
use App\Http\Controllers\StripeController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Route::post("/login",[AuthController::class,"login"]);
Route::post("/register",[AuthController::class,"register"]);
Route::middleware('auth:sanctum')->post('/addOrder', [OrderController::class, 'addOrder']);
Route::middleware('auth:sanctum')->post('/pay', [StripeController::class, 'pay']);
Route::middleware('auth:sanctum')->get('/displayOrders', [DisplayController::class, 'displayOrders']);
Route::get('/email/verify/{id}/{hash}', [AuthController::class, 'verifyEmail'])
->middleware(['signed'])
->name('verification.verify');
And these are my web routes
<?php
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use App\Http\Controllers\AuthController;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('welcome');
});
Route::get('/login', function () {
return redirect('http://localhost:5173/login');
})->name('login');
Route::get('/email/verify/{id}/{hash}', [AuthController::class, 'verifyEmail'])
->middleware(['auth:sanctum', 'signed'])
->name('verification.verify');
I already made sure that the user model has the email verification thing enabled. Simply , after I receive the confirmation email on mailtrap and click the button I am getting redirected to the login page and then when checking the data base the "email_verified_at" entry is still empty... pls help
Share Improve this question edited Mar 21 at 1:23 desertnaut 60.5k32 gold badges155 silver badges181 bronze badges asked Mar 21 at 0:23 TinoOoTinoOo 13 bronze badges1 Answer
Reset to default 0Your email_verified_at stays null because the verification link likely hits the web.php route with auth:sanctum, which fails since the user isn’t logged in. Remove that route and use only api.php with signed middleware. Simplify verifyEmail like this:
// api.php
Route::get('/email/verify/{id}/{hash}', [AuthController::class, 'verifyEmail'])
->middleware('signed')
->name('verification.verify');
// AuthController.php
public function verifyEmail(EmailVerificationRequest $request)
{
$request->fulfill();
return response()->json(['message' => 'Email verified!']);
}
Ensure User implements MustVerifyEmail. Test the link (http://localhost:8000/api/email/verify/...)—it should update the field now. Check laravel.log if it doesn’t.