set forget password backend
This commit is contained in:
parent
309934c18f
commit
fa299bcd7c
|
|
@ -1,41 +1,112 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use App\Mail\OtpEmail;
|
||||
use App\Models\User;
|
||||
|
||||
class ForgotPasswordController extends Controller
|
||||
class ForgotPasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset link request view.
|
||||
*/
|
||||
public function showLinkRequestForm(): View
|
||||
public function index()
|
||||
{
|
||||
return view('ForgotPassword');
|
||||
return view('forgotPassword');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a reset link to the given user.
|
||||
*/
|
||||
public function sendResetLinkEmail(Request $request): RedirectResponse
|
||||
public function resetEmail(Request $request)
|
||||
{
|
||||
// 1. Validate the incoming email address
|
||||
$request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'email' => 'required|email|exists:users,email'
|
||||
], [
|
||||
'email.exists' => 'The provided email is not registered in our system.'
|
||||
]);
|
||||
|
||||
// 2. Attempt to send the password reset link via Laravel Broker
|
||||
$status = Password::sendResetLink(
|
||||
$request->only('email')
|
||||
);
|
||||
$email = $request->input('email');
|
||||
$user = User::where('email', $email)->first();
|
||||
|
||||
// 3. Redirect back with status message or validation error
|
||||
return $status === Password::RESET_LINK_SENT
|
||||
? back()->with('status', __($status))
|
||||
: back()->withErrors(['email' => __($status)]);
|
||||
$otp = random_int(100000, 999999);
|
||||
|
||||
Session::put('otp', $otp);
|
||||
Session::put('email', $email);
|
||||
|
||||
$user->email_otp = $otp;
|
||||
$user->save();
|
||||
|
||||
Mail::to($email)->send(new OtpEmail($otp));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'OTP has been sent to your email.'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function verifyOtpEmail(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'otp' => 'required|numeric'
|
||||
]);
|
||||
|
||||
$otp = $request->input('otp');
|
||||
$email = Session::get('email');
|
||||
|
||||
if (!$email) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Session expired. Please request a new OTP.'
|
||||
], 400);
|
||||
}
|
||||
|
||||
$user = User::where('email', $email)
|
||||
->where('email_otp', $otp)
|
||||
->first();
|
||||
|
||||
if ($user) {
|
||||
Session::put('otp_verified', true);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'OTP is correct.',
|
||||
'email' => $email
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Invalid OTP code. Please try again.'
|
||||
], 422);
|
||||
}
|
||||
|
||||
public function updatePassword(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email|exists:users,email',
|
||||
'password_mo' => 'required|string|min:8|confirmed',
|
||||
]);
|
||||
|
||||
$email = $request->input('email');
|
||||
|
||||
if (!Session::get('otp_verified') || Session::get('email') !== $email) {
|
||||
return redirect()->back()->with('error', 'Unauthorized access or session expired.');
|
||||
}
|
||||
|
||||
$user = User::where('email', $email)->first();
|
||||
|
||||
if ($user) {
|
||||
$user->password = Hash::make($request->input('password_mo'));
|
||||
$user->email_otp = null;
|
||||
$user->save();
|
||||
|
||||
Session::forget(['otp', 'email', 'otp_verified']);
|
||||
|
||||
return redirect('/signin')->with('success', 'Password updated successfully. Please login.');
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', 'User not found.');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Attachment;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class OtpEmail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $otp;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($otp)
|
||||
{
|
||||
$this->otp = $otp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Academy - Password Reset OTP',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'Mail.otp',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ class User extends Authenticatable
|
|||
'email',
|
||||
'phone',
|
||||
'password',
|
||||
'email_otp',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('email_otp')->nullable()->after('password');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('email_otp');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -3,33 +3,32 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ session('step') == 'otp' ? 'Verify OTP' : 'Forgot Password' }}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Password Recovery</title>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
|
||||
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: white;
|
||||
background: #f4f6f9;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
.container-box {
|
||||
background: #ffffff;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
border-radius: 16px;
|
||||
padding: 40px 36px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.icon-wrap {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
|
@ -40,288 +39,399 @@
|
|||
justify-content: center;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
.icon-wrap svg {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
stroke: #eeff8f;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
color: #1f2937;
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p.subtitle {
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
margin-bottom: 28px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #047857;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #dc2626;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
input[type="email"] {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1.5px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
.icon-wrap i { font-size: 24px; color: #eeff8f; }
|
||||
h1 { font-size: 22px; color: #1f2937; text-align: center; margin-bottom: 8px; font-weight: 700; }
|
||||
p.subtitle { text-align: center; color: #6b7280; font-size: 14px; margin-bottom: 28px; line-height: 1.5; }
|
||||
.form-group { margin-bottom: 20px; position: relative; }
|
||||
label { display: block; font-size: 13px; font-weight: 600; color: #374151; margin-bottom: 6px; }
|
||||
input[type="email"], input[type="text"], input[type="password"] {
|
||||
width: 100%; padding: 12px 14px; border: 1.5px solid #e5e7eb;
|
||||
border-radius: 10px; font-size: 14px; color: #1f2937; outline: none;
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="email"]:focus {
|
||||
border-color: #8b5cf6;
|
||||
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
|
||||
}
|
||||
|
||||
input[type="email"]::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc2626;
|
||||
font-size: 12.5px;
|
||||
margin-top: 6px;
|
||||
input:focus { border-color: #943835e3; box-shadow: 0 0 0 3px rgba(148, 56, 53, 0.15); }
|
||||
.toggle-password { position: absolute; right: 14px; top: 38px; cursor: pointer; color: #6b7280; }
|
||||
.err, .error { color: #dc2626; font-size: 12.5px; margin-top: 6px; display: block; }
|
||||
.otp-input { letter-spacing: 4px; text-align: center; font-size: 20px; font-weight: 700; }
|
||||
button[type="submit"] {
|
||||
width: 100%; padding: 13px; background: #943835e3; color: #fff;
|
||||
border: none; border-radius: 10px; font-size: 15px; font-weight: 600;
|
||||
cursor: pointer; margin-top: 10px; transition: transform 0.15s ease;
|
||||
}
|
||||
button[type="submit"]:hover { transform: translateY(-1px); box-shadow: 0 8px 20px rgba(148, 56, 53, 0.35); }
|
||||
button[type="submit"]:disabled { background: #94383580; cursor: not-allowed; }
|
||||
.back-link { display: block; text-align: center; margin-top: 22px; font-size: 13.5px; color: #6b7280; }
|
||||
.back-link a { color: #231d77e3; font-weight: 600; text-decoration: none; }
|
||||
.powered { text-align: center; margin-top: 20px; font-size: 12px; color: #9ca3af; }
|
||||
|
||||
.otp-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.otp-input {
|
||||
.otp-box {
|
||||
width: 48px;
|
||||
height: 52px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
border: 1.5px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.otp-input:focus {
|
||||
.otp-box:focus {
|
||||
border-color: #943835e3;
|
||||
box-shadow: 0 0 0 3px rgba(148, 56, 53, 0.15);
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
.success-icon-wrap {
|
||||
font-size: 60px;
|
||||
color: #10b981; /* Green color */
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.login-btn-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 13px;
|
||||
background: #943835e3;
|
||||
color: #fff;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
margin-top: 20px;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
button[type="submit"]:hover {
|
||||
.login-btn-link:hover {
|
||||
color: #fff;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 20px rgba(148, 56, 53, 0.35);
|
||||
}
|
||||
|
||||
button[type="submit"]:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.resend-box {
|
||||
text-align: center;
|
||||
margin-top: 22px;
|
||||
font-size: 13.5px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.resend-box button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #231d77e3;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 22px;
|
||||
font-size: 13.5px;
|
||||
color: #6b7280;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
color: #231d77e3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
{{-- SUCCESS & ERROR ALERTS --}}
|
||||
@if (session('status'))
|
||||
<div class="alert-success">{{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
@if ($errors->has('otp'))
|
||||
<div class="alert-danger">{{ $errors->first('otp') }}</div>
|
||||
@endif
|
||||
|
||||
{{-- SECTION 2: VERIFY OTP (Shows when session step is 'otp') --}}
|
||||
@if (session('step') === 'otp')
|
||||
<div class="icon-wrap">
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751A11.959 11.959 0 0112 2.714z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1>Enter OTP Code</h1>
|
||||
<p class="subtitle">We've sent a 6-digit verification code to <br><strong>{{ session('email', old('email')) }}</strong></p>
|
||||
|
||||
<form method="POST" action="{{ route('password.verify.otp') }}" id="otp-form">
|
||||
@csrf
|
||||
<input type="hidden" name="email" value="{{ session('email', old('email')) }}">
|
||||
<input type="hidden" name="otp" id="full-otp">
|
||||
|
||||
<div class="otp-container">
|
||||
<input type="text" maxlength="1" class="otp-input" autofocus autocomplete="off">
|
||||
<input type="text" maxlength="1" class="otp-input" autocomplete="off">
|
||||
<input type="text" maxlength="1" class="otp-input" autocomplete="off">
|
||||
<input type="text" maxlength="1" class="otp-input" autocomplete="off">
|
||||
<input type="text" maxlength="1" class="otp-input" autocomplete="off">
|
||||
<input type="text" maxlength="1" class="otp-input" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<button type="submit">Verify & Continue</button>
|
||||
</form>
|
||||
|
||||
<div class="resend-box">
|
||||
Didn't receive the code?
|
||||
<form method="POST" action="{{ route('password.email') }}" style="display: inline;">
|
||||
@csrf
|
||||
<input type="hidden" name="email" value="{{ session('email', old('email')) }}">
|
||||
<button type="submit">Resend OTP</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- SECTION 1: FORGOT PASSWORD (Default view) --}}
|
||||
@else
|
||||
<div class="icon-wrap">
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1>Forgot Password?</h1>
|
||||
<p class="subtitle">No worries! Enter your email and we'll send you a link to reset your password.</p>
|
||||
|
||||
<form method="POST" action="{{ route('password.email') }}">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
value="{{ old('email') }}"
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
autofocus
|
||||
>
|
||||
@error('email')
|
||||
<div class="text-danger">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<button type="submit">Send OTP</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
<a href="/signin" class="back-link">← Back to Login</a>
|
||||
<div class="container-box">
|
||||
<div class="icon-wrap" id="main-icon">
|
||||
<i class="fa-solid fa-key"></i>
|
||||
</div>
|
||||
|
||||
@if (session('step') === 'otp')
|
||||
<script>
|
||||
const inputs = document.querySelectorAll('.otp-input');
|
||||
const form = document.getElementById('otp-form');
|
||||
const fullOtpInput = document.getElementById('full-otp');
|
||||
<!-- Step 1: Email Form -->
|
||||
<form class="login-form" name="login-form" action="{{ route('password.email') }}" method="post" id="login-form">
|
||||
@csrf
|
||||
<h1>Recover Password</h1>
|
||||
<p class="subtitle">Enter your registered email address to receive an OTP code.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email-login">Email Address</label>
|
||||
<input type="email" name="email" id="email-login" placeholder="name@example.com" required>
|
||||
<span id="email-err" class="err"></span>
|
||||
@if ($errors->has('email'))
|
||||
<span class="err">{{ $errors->first('email') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<button type="submit" id="verify-btn" onclick="validateEmail(event)">Verify Email</button>
|
||||
|
||||
<div class="back-link">
|
||||
Remember the password? <a href="/signin">Please Login</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Step 2: OTP Verification Form -->
|
||||
<div id="otp_sec" style="display: none;">
|
||||
<h1>Enter OTP Code</h1>
|
||||
<p class="subtitle">Please check your email and enter the 6-digit verification code.</p>
|
||||
|
||||
<form name="emailOtpForm" id="emailOtpForm">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label>Verification Code</label>
|
||||
|
||||
<div class="otp-container">
|
||||
<input type="text" class="otp-box" maxlength="1" pattern="\d*" inputmode="numeric" required>
|
||||
<input type="text" class="otp-box" maxlength="1" pattern="\d*" inputmode="numeric" required>
|
||||
<input type="text" class="otp-box" maxlength="1" pattern="\d*" inputmode="numeric" required>
|
||||
<input type="text" class="otp-box" maxlength="1" pattern="\d*" inputmode="numeric" required>
|
||||
<input type="text" class="otp-box" maxlength="1" pattern="\d*" inputmode="numeric" required>
|
||||
<input type="text" class="otp-box" maxlength="1" pattern="\d*" inputmode="numeric" required>
|
||||
</div>
|
||||
<input type="hidden" id="otp" name="otpm">
|
||||
|
||||
<span id="otpm_err" class="err"></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" onclick="showPassword(event)">Submit Code</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Password Reset Form -->
|
||||
<div id="password_sec" style="display: none;">
|
||||
<h1>Reset Password</h1>
|
||||
<p class="subtitle">Create a new password for your account.</p>
|
||||
|
||||
<form class="login-form" name="reset-password-form" id="reset-password-form" action="{{ route('password.update') }}" method="post" onsubmit="handlePasswordReset(event)">
|
||||
@csrf
|
||||
<input type="hidden" name="email" value="" id="hidden_email">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password-m">New Password</label>
|
||||
<input type="password" name="password_mo" id="password-m" placeholder="••••••••" required>
|
||||
<i class="fa-solid fa-eye toggle-password" id="view1" onclick="togglePassword('password-m', 'view1', 'no-view1')"></i>
|
||||
<i class="fa-solid fa-eye-slash toggle-password" id="no-view1" onclick="togglePassword('password-m', 'view1', 'no-view1')" style="display:none;"></i>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">Confirm Password</label>
|
||||
<input type="password" name="password_mo_confirmation" id="confirm_password" placeholder="••••••••" required>
|
||||
<i class="fa-solid fa-eye toggle-password" id="view-con1" onclick="togglePassword('confirm_password', 'view-con1', 'no-view-con1')"></i>
|
||||
<i class="fa-solid fa-eye-slash toggle-password" id="no-view-con1" onclick="togglePassword('confirm_password', 'view-con1', 'no-view-con1')" style="display:none;"></i>
|
||||
<span id="con-pass-err" class="error"></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="reset-btn">Reset Password</button>
|
||||
|
||||
<div class="back-link">
|
||||
Remember password? <a href="/signin">Please Login</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Success Message Screen -->
|
||||
<div id="success_sec" style="display: none; text-align: center;">
|
||||
<div class="success-icon-wrap">
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
</div>
|
||||
<h1>Password Reset Successful!</h1>
|
||||
<p class="subtitle">Your password has been reset successfully. You can now log in with your new password.</p>
|
||||
|
||||
<a href="/signin" class="login-btn-link">Proceed to Login</a>
|
||||
</div>
|
||||
|
||||
<p class="powered">Powered by <a href="#" style="color: #6b7280; text-decoration: none; font-weight: 600;">GPiT</a></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function validateEmail(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var email = document.getElementById('email-login').value;
|
||||
var emailErr = document.getElementById('email-err');
|
||||
var csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
var verifyBtn = document.getElementById('verify-btn');
|
||||
|
||||
emailErr.textContent = '';
|
||||
|
||||
if (email.trim() === '') {
|
||||
emailErr.textContent = 'Please enter your email';
|
||||
return;
|
||||
}
|
||||
|
||||
verifyBtn.disabled = true;
|
||||
verifyBtn.textContent = 'Please Wait...';
|
||||
|
||||
fetch("{{ route('password.email') }}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ email: email })
|
||||
})
|
||||
.then(async response => {
|
||||
const text = await response.text();
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
console.error("SERVER ERROR DETAILS:", text);
|
||||
throw new Error("Server returned HTML response instead of JSON.");
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById("hidden_email").value = email;
|
||||
document.getElementById("login-form").style.display = "none";
|
||||
document.getElementById("otp_sec").style.display = "block";
|
||||
} else {
|
||||
if (data.errors && data.errors.email) {
|
||||
emailErr.textContent = data.errors.email[0];
|
||||
} else {
|
||||
emailErr.textContent = data.message || 'Email not found in our records.';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
emailErr.textContent = 'An error occurred. Check browser console (F12) for details.';
|
||||
})
|
||||
.finally(() => {
|
||||
verifyBtn.disabled = false;
|
||||
verifyBtn.textContent = 'Verify Email';
|
||||
});
|
||||
}
|
||||
|
||||
function showPassword(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const otp = document.getElementById('otp').value;
|
||||
const otpm_err = document.getElementById('otpm_err');
|
||||
const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
|
||||
otpm_err.innerText = '';
|
||||
|
||||
if (otp.trim() === '' || otp.length < 6) {
|
||||
otpm_err.innerText = 'Please enter a valid 6-digit OTP code.';
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("{{ route('password.verify.otp') }}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': token
|
||||
},
|
||||
body: JSON.stringify({ otp: otp })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById("otp_sec").style.display = "none";
|
||||
document.getElementById("password_sec").style.display = "block";
|
||||
} else {
|
||||
otpm_err.innerText = data.message || 'Invalid OTP code';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
otpm_err.innerText = 'Verification failed. Try again.';
|
||||
});
|
||||
}
|
||||
|
||||
function handlePasswordReset(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const form = document.getElementById('reset-password-form');
|
||||
const formData = new FormData(form);
|
||||
const resetBtn = document.getElementById('reset-btn');
|
||||
const passErr = document.getElementById('con-pass-err');
|
||||
|
||||
const pass = document.getElementById('password-m').value;
|
||||
const confirmPass = document.getElementById('confirm_password').value;
|
||||
|
||||
if (pass !== confirmPass) {
|
||||
passErr.innerText = "Passwords do not match!";
|
||||
passErr.style.color = "#dc2626";
|
||||
return;
|
||||
}
|
||||
|
||||
resetBtn.disabled = true;
|
||||
resetBtn.textContent = 'Resetting...';
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async response => {
|
||||
const data = await response.json();
|
||||
if (response.ok || data.success) {
|
||||
|
||||
document.getElementById("main-icon").style.display = "none";
|
||||
document.getElementById("password_sec").style.display = "none";
|
||||
document.getElementById("success_sec").style.display = "block";
|
||||
} else {
|
||||
passErr.innerText = data.message || 'Failed to reset password.';
|
||||
passErr.style.color = "#dc2626";
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
// Unexpected redirect/success response handling
|
||||
document.getElementById("main-icon").style.display = "none";
|
||||
document.getElementById("password_sec").style.display = "none";
|
||||
document.getElementById("success_sec").style.display = "block";
|
||||
})
|
||||
.finally(() => {
|
||||
resetBtn.disabled = false;
|
||||
resetBtn.textContent = 'Reset Password';
|
||||
});
|
||||
}
|
||||
|
||||
$('#password-m, #confirm_password').on('keyup', function() {
|
||||
if ($('#password-m').val() !== '' && $('#password-m').val() === $('#confirm_password').val()) {
|
||||
$('#con-pass-err').html('Passwords match').css('color', '#047857');
|
||||
} else if ($('#confirm_password').val() !== '') {
|
||||
$('#con-pass-err').html('Passwords do not match').css('color', '#dc2626');
|
||||
}
|
||||
});
|
||||
|
||||
function togglePassword(inputId, eyeId, eyeSlashId) {
|
||||
var field = document.getElementById(inputId);
|
||||
var eye = document.getElementById(eyeId);
|
||||
var eyeSlash = document.getElementById(eyeSlashId);
|
||||
|
||||
if (field.type === "password") {
|
||||
field.type = "text";
|
||||
eye.style.display = "none";
|
||||
eyeSlash.style.display = "block";
|
||||
} else {
|
||||
field.type = "password";
|
||||
eye.style.display = "block";
|
||||
eyeSlash.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const inputs = document.querySelectorAll(".otp-box");
|
||||
const hiddenOtpInput = document.getElementById("otp");
|
||||
|
||||
inputs.forEach((input, index) => {
|
||||
input.addEventListener('input', (e) => {
|
||||
if (e.target.value.length === 1 && index < inputs.length - 1) {
|
||||
input.addEventListener("input", (e) => {
|
||||
input.value = input.value.replace(/\D/g, '');
|
||||
|
||||
if (input.value && index < inputs.length - 1) {
|
||||
inputs[index + 1].focus();
|
||||
}
|
||||
updateHiddenOtp();
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Backspace' && !e.target.value && index > 0) {
|
||||
input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Backspace" && !input.value && index > 0) {
|
||||
inputs[index - 1].focus();
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('paste', (e) => {
|
||||
const pasteData = e.clipboardData.getData('text').trim();
|
||||
if (pasteData.length === inputs.length) {
|
||||
pasteData.split('').forEach((char, i) => {
|
||||
inputs[i].value = char;
|
||||
input.addEventListener("paste", (e) => {
|
||||
e.preventDefault();
|
||||
const pasteData = e.clipboardData.getData("text").trim().replace(/\D/g, '');
|
||||
if (pasteData.length === 6) {
|
||||
pasteData.split("").forEach((char, i) => {
|
||||
if (inputs[i]) inputs[i].value = char;
|
||||
});
|
||||
inputs[inputs.length - 1].focus();
|
||||
inputs[5].focus();
|
||||
updateHiddenOtp();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
form.addEventListener('submit', (e) => {
|
||||
let otpValue = '';
|
||||
inputs.forEach(input => otpValue += input.value);
|
||||
fullOtpInput.value = otpValue;
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
function updateHiddenOtp() {
|
||||
let fullOtp = "";
|
||||
inputs.forEach(input => fullOtp += input.value);
|
||||
hiddenOtpInput.value = fullOtp;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Password Reset OTP - Academy</title>
|
||||
<style>
|
||||
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
||||
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
|
||||
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
|
||||
body { height: 100% !important; margin: 0 !important; padding: 0 !important; width: 100% !important; background-color: #f4f7f6; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #f4f7f6;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 10px;">
|
||||
<!-- Main Container -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 550px; background-color: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.05);">
|
||||
|
||||
<!-- Header / Logo Area -->
|
||||
<tr>
|
||||
<td align="center" style="padding: 30px 20px; background-color: #1a2b4c;">
|
||||
<!-- Academy Logo / Branding -->
|
||||
<h2 style="color: #ffffff; margin: 0; font-size: 24px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase;">
|
||||
ACADEMY
|
||||
</h2>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Body Content -->
|
||||
<tr>
|
||||
<td style="padding: 40px 30px; text-align: center;">
|
||||
<h1 style="color: #20396b; font-size: 22px; font-weight: 700; margin-top: 0; margin-bottom: 12px;">
|
||||
Reset Your Password
|
||||
</h1>
|
||||
|
||||
<p style="color: #683636; font-size: 15px; line-height: 22px; margin-bottom: 25px;">
|
||||
Hello,<br>
|
||||
We received a request to reset the password for your Academy account. Use the Verification Code (OTP) below to proceed.
|
||||
</p>
|
||||
|
||||
<!-- OTP Box -->
|
||||
<div style="background-color: #f0f4f9; border: 2px dashed #0236b9; border-radius: 8px; padding: 18px; margin: 20px 0; display: inline-block; width: 80%;">
|
||||
<span style="font-size: 32px; font-weight: 800; color: #0236b9; letter-spacing: 6px; font-family: 'Courier New', Courier, monospace;">
|
||||
{{ $otp }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p style="color: #888888; font-size: 13px; line-height: 18px; margin-top: 25px;">
|
||||
This OTP is valid for <strong>10 minutes</strong>. If you did not request a password reset, please ignore this email.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Divider -->
|
||||
<tr>
|
||||
<td style="padding: 0 30px;">
|
||||
<hr style="border: none; border-top: 1px solid #eeeeee; margin: 0;">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="padding: 25px 30px; text-align: center; background-color: #fafafa;">
|
||||
<p style="color: #999999; font-size: 12px; margin: 0 0 8px 0;">
|
||||
Regards,<br>
|
||||
<strong>Academy Learning Team</strong>
|
||||
</p>
|
||||
<p style="color: #cccccc; font-size: 11px; margin: 0;">
|
||||
© {{ date('Y') }} Academy. All rights reserved.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
}
|
||||
|
||||
.btn-primary:hover, .btn-primary:focus {
|
||||
background: var(--theme-red-hover) !important;
|
||||
background: #822424 !important;
|
||||
box-shadow: 0 4px 12px rgba(130, 36, 36, 0.3);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,37 +22,23 @@ Route::get('/', function () {
|
|||
});
|
||||
|
||||
|
||||
|
||||
|
||||
Route::get('forgot-password', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request');
|
||||
Route::post('forgot-password', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
|
||||
Route::middleware('guest')->group(function () {
|
||||
|
||||
Route::get('/password/reset', [ForgotPasswordController::class, 'index'])->name('password.request');
|
||||
Route::post('/resetEmail', [ForgotPasswordController::class, 'resetEmail'])->name('password.email');
|
||||
|
||||
// Route::get('password/reset', [ForgotPasswordController::class, 'showLinkRequestForm'])
|
||||
// ->name('password.request');
|
||||
Route::post('/verify-otp', [ForgotPasswordController::class, 'verifyOtpEmail'])->name('password.verify.otp');
|
||||
|
||||
// Route::post('password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])
|
||||
// ->name('password.email');
|
||||
|
||||
Route::post('/update-password', [ForgotPasswordController::class, 'updatePassword'])->name('password.update');
|
||||
});
|
||||
|
||||
// Route::get('password/reset/{token}', [ForgotPasswordController::class, 'showResetForm'])
|
||||
// ->name('password.reset');
|
||||
|
||||
// Route::post('password/reset', [ForgotPasswordController::class, 'reset'])
|
||||
// ->name('password.update');
|
||||
|
||||
|
||||
|
||||
|
||||
// Password Reset Routes
|
||||
Route::get('password/reset', [ForgotPasswordController::class, 'showLinkRequestForm'])
|
||||
->name('password.request');
|
||||
|
||||
Route::post('password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])
|
||||
->name('password.email');
|
||||
|
||||
Route::get('password/reset/{token}', [ForgotPasswordController::class, 'showResetForm'])
|
||||
->name('password.reset');
|
||||
|
||||
Route::post('password/reset', [ForgotPasswordController::class, 'reset'])
|
||||
->name('password.update');
|
||||
|
||||
Route::post('/studentlogout', [studentportalnavController::class, 'logout'])->name('student.logout');
|
||||
|
||||
Route::get('/StudentProfile', [StudentPortalController::class, 'showProfile'])->name('StudentProfile');
|
||||
|
|
|
|||
Loading…
Reference in New Issue