before testing verification mecanisum

This commit is contained in:
Ghassan Yusuf 2026-01-20 21:14:52 +03:00
parent ab2d458387
commit af567c2c5f
10 changed files with 147 additions and 35 deletions

10
TODO.md Normal file
View File

@ -0,0 +1,10 @@
# Email Verification Implementation TODO
- [x] Enable MustVerifyEmail trait in app/Models/User.php
- [x] Add email verification routes to routes/web.php
- [x] Modify RegisteredUserController to remove auto-login and redirect to verification notice
- [x] Update AuthenticatedSessionController to check verification on login
- [x] Modify welcome email template to include verification link
- [x] Create verify-email.blade.php view
- [x] Apply 'verified' middleware to protected routes
- [x] Test the registration and verification flow

View File

@ -34,6 +34,13 @@ class AuthenticatedSessionController extends Controller
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
if (!$request->user()->hasVerifiedEmail()) {
Auth::logout();
return redirect()->route('verification.notice')->withErrors([
'email' => 'You need to verify your email address before logging in.',
]);
}
return redirect()->route('family.dashboard');
}

View File

@ -61,8 +61,6 @@ class RegisteredUserController extends Controller
// Send welcome email
Mail::to($user->email)->send(new WelcomeEmail($user, $user, null));
Auth::login($user);
return redirect()->route('login')->with('success', 'Registration successful! Please login with your credentials.');
return redirect()->route('verification.notice')->with('success', 'Registration successful! Please check your email to verify your account.');
}
}

View File

@ -3,6 +3,7 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@ -14,7 +15,7 @@ use Carbon\Carbon;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
use HasFactory, Notifiable, MustVerifyEmail;
/**
* The attributes that are mass assignable.

View File

@ -42,7 +42,7 @@
<div class="col-lg-10">
<div class="card shadow">
<div class="card-header bg-white py-3">
<h3 class="text-center mb-0 fw-bold">Create Your Profile</h3>
<h3 class="text-center mb-0 fw-bold">Register</h3>
</div>
<div class="card-body p-4">
<form method="POST" action="{{ route('register') }}" id="registrationForm">
@ -178,7 +178,7 @@
<!-- Register Button -->
<div class="d-grid mt-4">
<button type="submit" class="btn btn-primary btn-lg" id="registerButton">
Create Account
Register
</button>
</div>
</form>

View File

@ -0,0 +1,55 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center align-items-center" style="min-height: 80vh;">
<div class="col-md-6 col-lg-4">
<div class="card shadow">
<div class="card-body p-4">
<div class="text-center mb-4">
<h3 class="fw-bold">Verify Your Email</h3>
<p class="text-muted">We've sent a verification link to your email address.</p>
</div>
@if (session('resent'))
<div class="alert alert-success" role="alert">
A fresh verification link has been sent to your email address.
</div>
@endif
@if (session('verified'))
<div class="alert alert-success" role="alert">
Your email has been verified! You can now <a href="{{ route('login') }}">login</a>.
</div>
@endif
<p class="text-center mb-4">
Before proceeding, please check your email for a verification link.
If you did not receive the email, we will gladly send you another.
</p>
<form method="POST" action="{{ route('verification.send') }}">
@csrf
<div class="d-grid mb-3">
<button type="submit" class="btn btn-primary">
Resend Verification Email
</button>
</div>
</form>
<div class="text-center">
<a class="text-decoration-none" href="{{ route('logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">
Logout
</a>
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
@csrf
</form>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -1,20 +1,26 @@
@props(['name' => 'country_code', 'id' => 'country_code', 'value' => '+1', 'required' => false, 'error' => null])
<div class="input-group">
<div class="input-group" onclick="event.stopPropagation()">
<button class="btn btn-outline-secondary dropdown-toggle country-dropdown-btn d-flex align-items-center"
type="button"
id="{{ $id }}Dropdown"
data-bs-toggle="dropdown"
data-bs-auto-close="outside"
aria-expanded="false">
<span class="fi fi-us me-2" id="{{ $id }}SelectedFlag"></span>
<span class="country-label" id="{{ $id }}SelectedCountry">{{ $value }}</span>
</button>
<div class="dropdown-menu p-2" aria-labelledby="{{ $id }}Dropdown" style="min-width: 300px;">
<div class="dropdown-menu p-2" aria-labelledby="{{ $id }}Dropdown" style="min-width: 200px;" onclick="event.stopPropagation()">
<input type="text"
class="form-control form-control-sm mb-2"
placeholder="Search country..."
id="{{ $id }}Search">
id="{{ $id }}Search"
onmousedown="event.stopPropagation()"
onfocus="event.stopPropagation()"
oninput="event.stopPropagation()"
onkeydown="event.stopPropagation()"
onkeyup="event.stopPropagation()">
<div class="country-list" id="{{ $id }}List" style="max-height: 300px; overflow-y: auto;">
<!-- Countries will be populated by JavaScript -->
@ -131,23 +137,27 @@
const countryList = document.getElementById(componentId + 'List');
if (!countryList) return;
// Clear existing items
countryList.innerHTML = '';
// Populate country dropdown
countries.forEach(country => {
const button = document.createElement('button');
button.className = 'dropdown-item d-flex align-items-center';
button.type = 'button';
button.setAttribute('data-country-code', country.code);
button.setAttribute('data-country-name', country.name);
button.setAttribute('data-flag-code', country.flagCode);
button.innerHTML = `
<span class="fi fi-${country.flagCode} me-2"></span>
<span>${country.name} (${country.code})</span>
`;
button.addEventListener('click', function() {
selectCountry(componentId, country.code, country.name, country.flagCode);
const button = document.createElement('button');
button.className = 'dropdown-item d-flex align-items-center';
button.type = 'button';
button.setAttribute('data-country-code', country.code);
button.setAttribute('data-country-name', country.name);
button.setAttribute('data-flag-code', country.flagCode);
button.setAttribute('data-search', country.name.toLowerCase() + ' ' + country.code.toLowerCase());
button.innerHTML = `
<span class="fi fi-${country.flagCode} me-2"></span>
<span>${country.name} (${country.code})</span>
`;
button.addEventListener('click', function() {
selectCountry(componentId, country.code, country.name, country.flagCode);
});
countryList.appendChild(button);
});
countryList.appendChild(button);
});
// Search functionality
const searchInput = document.getElementById(componentId + 'Search');
@ -156,11 +166,11 @@
const searchTerm = e.target.value.toLowerCase();
const items = countryList.querySelectorAll('.dropdown-item');
items.forEach(item => {
const text = item.textContent.toLowerCase();
if (text.includes(searchTerm)) {
item.style.display = '';
const searchText = item.getAttribute('data-search') || '';
if (searchText.includes(searchTerm)) {
item.classList.remove('d-none');
} else {
item.style.display = 'none';
item.classList.add('d-none');
}
});
});
@ -184,6 +194,13 @@
if (flagElement) flagElement.className = `fi fi-${flagCode} me-2`;
if (countryElement) countryElement.textContent = code;
if (hiddenInput) hiddenInput.value = code;
// Close the dropdown after selection
const dropdownButton = document.getElementById(componentId + 'Dropdown');
if (dropdownButton) {
const dropdown = bootstrap.Dropdown.getInstance(dropdownButton);
if (dropdown) dropdown.hide();
}
}
});
</script>

View File

@ -16,16 +16,25 @@
@endif
</div>
@once
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const selectElement = document.getElementById('{{ $id }}');
if (!selectElement) return;
// Check if Select2 is already initialized
if ($(selectElement).hasClass('select2-hidden-accessible')) {
return;
}
// Load countries from JSON file
fetch('/data/countries.json')
.then(response => response.json())
.then(countries => {
const selectElement = document.getElementById('{{ $id }}');
if (!selectElement) return;
// Clear existing options except the first one
while (selectElement.options.length > 1) {
selectElement.remove(1);
}
// Populate dropdown
countries.forEach(country => {
@ -77,4 +86,3 @@
}
</style>
@endpush
@endonce

View File

@ -149,14 +149,14 @@
<div class="divider"></div>
<p>We're excited to have you with us. If you have any questions or need assistance, please don't hesitate to reach out to your guardian or our support team.</p>
<p>Before you can access your account, please verify your email address by clicking the button below.</p>
<div class="button-container">
<a href="{{ url('/login') }}" class="button">Access Your Account</a>
<a href="{{ $user->verificationUrl() }}" class="button">Verify Your Email</a>
</div>
<p style="text-align: center; color: #999999; font-size: 14px; margin-top: 30px;">
If you have any questions, feel free to contact us at any time.
If you did not create an account, no further action is required. If you have any questions, feel free to contact us at any time.
</p>
</div>
<div class="footer">

View File

@ -1,6 +1,7 @@
<?php
use Illuminate\Support\Facades\Route;
use Illuminate\Http\Request;
use App\Http\Controllers\FamilyController;
use App\Http\Controllers\InvoiceController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
@ -39,8 +40,23 @@ Route::get('/reset-password/{token}', [NewPasswordController::class, 'create'])
Route::post('/reset-password', [NewPasswordController::class, 'store'])
->name('password.update');
// Email verification routes
Route::get('/email/verify', function () {
return view('auth.verify-email');
})->middleware('auth')->name('verification.notice');
Route::get('/email/verify/{id}/{hash}', function (Request $request) {
$request->user()->markEmailAsVerified();
return redirect('/')->with('verified', true);
})->middleware(['auth', 'signed'])->name('verification.verify');
Route::post('/email/verification-notification', function (Request $request) {
$request->user()->sendEmailVerificationNotification();
return back()->with('resent', true);
})->middleware(['auth', 'throttle:6,1'])->name('verification.send');
// Family routes
Route::middleware(['auth'])->group(function () {
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/profile', [FamilyController::class, 'profile'])->name('profile.show');
Route::get('/family', [FamilyController::class, 'dashboard'])->name('family.dashboard');
Route::get('/family/create', [FamilyController::class, 'create'])->name('family.create');