update lupa pw
This commit is contained in:
parent
6ce214315c
commit
aa29baab24
|
|
@ -10,14 +10,24 @@
|
|||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
use Carbon\Carbon;
|
||||
use App\Services\FonnteService;
|
||||
|
||||
class AdminForgotPasswordController extends Controller
|
||||
{
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// STEP 1 — Tampilkan form input email
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
||||
protected FonnteService $fonnte;
|
||||
|
||||
public function __construct(FonnteService $fonnte)
|
||||
{
|
||||
$this->fonnte = $fonnte;
|
||||
}
|
||||
|
||||
public function showForgotForm()
|
||||
{
|
||||
|
||||
return view('admin.forgot-password');
|
||||
}
|
||||
|
||||
|
|
@ -25,6 +35,17 @@ public function showForgotForm()
|
|||
// STEP 1 — Proses: validasi email & kirim OTP
|
||||
// ─────────────────────────────────────────────────────────
|
||||
public function sendOtp(Request $request)
|
||||
{
|
||||
$method = $request->input('method', 'email');
|
||||
|
||||
if ($method === 'whatsapp') {
|
||||
return $this->sendOtpViaWhatsapp($request);
|
||||
}
|
||||
|
||||
return $this->sendOtpViaEmail($request);
|
||||
}
|
||||
|
||||
private function sendOtpViaEmail(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
|
|
@ -43,14 +64,7 @@ public function sendOtp(Request $request)
|
|||
])->withInput();
|
||||
}
|
||||
|
||||
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
|
||||
DB::table('password_reset_tokens')->where('email', $request->email)->delete();
|
||||
DB::table('password_reset_tokens')->insert([
|
||||
'email' => $request->email,
|
||||
'token' => Hash::make($otp),
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
$otp = $this->generateAndStoreOtp($request->email);
|
||||
|
||||
Mail::send('emails.admin-otp', [
|
||||
'otp' => $otp,
|
||||
|
|
@ -61,9 +75,74 @@ public function sendOtp(Request $request)
|
|||
});
|
||||
|
||||
$request->session()->put('admin_reset_email', $request->email);
|
||||
$request->session()->put('admin_reset_method', 'email');
|
||||
|
||||
return redirect()->route('admin.password.verify-otp-form')
|
||||
->with('success', 'Kode OTP telah dikirim ke email admin Anda. Berlaku 10 menit.');
|
||||
->with('success', 'Kode OTP telah dikirim ke email admin Anda. Berlaku 3 menit.');
|
||||
}
|
||||
|
||||
private function sendOtpViaWhatsapp(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'phone' => ['required', 'digits_between:10,15'],
|
||||
], [
|
||||
'phone.required' => 'Nomor WhatsApp wajib diisi.',
|
||||
'phone.digits_between' => 'Nomor WhatsApp tidak valid (10-15 digit).',
|
||||
]);
|
||||
|
||||
$phoneFormatted = FonnteService::formatPhone($request->phone);
|
||||
|
||||
// Cari user dengan nomor WA ini — WAJIB is_admin = true
|
||||
$admin = User::where('is_admin', true)
|
||||
->where(function ($q) use ($phoneFormatted, $request) {
|
||||
$q->where('phone', $phoneFormatted)
|
||||
->orWhere('phone', '+' . $phoneFormatted)
|
||||
->orWhere('phone', $request->phone)
|
||||
->orWhere('phone', '0' . ltrim($request->phone, '0'));
|
||||
})
|
||||
->first();
|
||||
|
||||
if (!$admin) {
|
||||
return back()->withErrors([
|
||||
'phone' => 'Nomor ini tidak terdaftar sebagai admin.',
|
||||
])->withInput();
|
||||
}
|
||||
|
||||
$otp = $this->generateAndStoreOtp($admin->email);
|
||||
|
||||
$pesan = "🛡️ *Bibit Cabai Bondowoso — Admin*\n\n"
|
||||
. "Halo {$admin->name},\n\n"
|
||||
. "Kode verifikasi reset password admin Anda:\n\n"
|
||||
. "*{$otp}*\n\n"
|
||||
. "Berlaku selama *3 menit*.\n"
|
||||
. "Jangan berikan kode ini kepada siapapun.";
|
||||
|
||||
$sent = $this->fonnte->sendMessage($phoneFormatted, $pesan);
|
||||
|
||||
if (!$sent) {
|
||||
return back()->with('error', 'Gagal mengirim pesan WhatsApp. Coba lagi.');
|
||||
}
|
||||
|
||||
$request->session()->put('admin_reset_email', $admin->email);
|
||||
$request->session()->put('admin_reset_method', 'whatsapp');
|
||||
$request->session()->put('admin_reset_phone_display', '****' . substr($request->phone, -4));
|
||||
|
||||
return redirect()->route('admin.password.verify-otp-form')
|
||||
->with('success', 'Kode OTP dikirim ke WhatsApp admin Anda. Berlaku 3 menit.');
|
||||
}
|
||||
|
||||
private function generateAndStoreOtp(string $email): string
|
||||
{
|
||||
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
|
||||
DB::table('password_reset_tokens')->where('email', $email)->delete();
|
||||
DB::table('password_reset_tokens')->insert([
|
||||
'email' => $email,
|
||||
'token' => Hash::make($otp),
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
return $otp;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
|
@ -134,30 +213,36 @@ public function verifyOtp(Request $request)
|
|||
public function resendOtp(Request $request)
|
||||
{
|
||||
$email = $request->session()->get('admin_reset_email');
|
||||
$method = $request->session()->get('admin_reset_method', 'email');
|
||||
|
||||
if (!$email) {
|
||||
return redirect()->route('admin.password.request')
|
||||
->with('error', 'Sesi tidak valid. Silakan masukkan email admin kembali.');
|
||||
->with('error', 'Sesi tidak valid.');
|
||||
}
|
||||
|
||||
$admin = User::where('email', $email)
|
||||
->where('is_admin', true)
|
||||
->first();
|
||||
|
||||
$admin = User::where('email', $email)->where('is_admin', true)->first();
|
||||
if (!$admin) {
|
||||
return redirect()->route('admin.password.request')
|
||||
->with('error', 'Email tidak ditemukan.');
|
||||
->with('error', 'Akun admin tidak ditemukan.');
|
||||
}
|
||||
|
||||
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
$otp = $this->generateAndStoreOtp($email);
|
||||
|
||||
DB::table('password_reset_tokens')->where('email', $email)->delete();
|
||||
DB::table('password_reset_tokens')->insert([
|
||||
'email' => $email,
|
||||
'token' => Hash::make($otp),
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
if ($method === 'whatsapp') {
|
||||
$phone = FonnteService::formatPhone(
|
||||
preg_replace('/^\+/', '', $admin->phone)
|
||||
);
|
||||
$pesan = "🛡️ *Bibit Cabai Bondowoso — Admin*\n\n"
|
||||
. "Halo {$admin->name},\n\n"
|
||||
. "Kode verifikasi baru Anda:\n\n"
|
||||
. "*{$otp}*\n\n"
|
||||
. "Berlaku selama *3 menit*.";
|
||||
|
||||
$sent = $this->fonnte->sendMessage($phone, $pesan);
|
||||
if (!$sent) {
|
||||
return back()->with('error', 'Gagal mengirim ulang ke WhatsApp.');
|
||||
}
|
||||
} else {
|
||||
Mail::send('emails.admin-otp', [
|
||||
'otp' => $otp,
|
||||
'name' => $admin->name ?? 'Admin',
|
||||
|
|
@ -165,8 +250,9 @@ public function resendOtp(Request $request)
|
|||
$message->to($email)
|
||||
->subject('Kode OTP Baru (Kirim Ulang) - Bibit Cabai Admin');
|
||||
});
|
||||
}
|
||||
|
||||
return back()->with('success', 'Kode OTP baru telah dikirim ke email admin Anda.');
|
||||
return back()->with('success', 'Kode OTP baru telah dikirim.');
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Services\FonnteService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
|
@ -13,18 +14,37 @@
|
|||
|
||||
class ForgotPasswordController extends Controller
|
||||
{
|
||||
// ─────────────────────────────────────────────
|
||||
// STEP 1 — Show form: enter email
|
||||
// ─────────────────────────────────────────────
|
||||
protected FonnteService $fonnte;
|
||||
|
||||
public function __construct(FonnteService $fonnte)
|
||||
{
|
||||
$this->fonnte = $fonnte;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// STEP 1 — Show form: pilih metode (email / WA)
|
||||
// ──────────────────────────────────────────────
|
||||
public function showForgotForm()
|
||||
{
|
||||
return view('auth.forgot-password');
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// STEP 1 — Handle: send OTP to email
|
||||
// ─────────────────────────────────────────────
|
||||
// ──────────────────────────────────────────────
|
||||
// STEP 1 — Handle: kirim OTP (email ATAU WA)
|
||||
// ──────────────────────────────────────────────
|
||||
public function sendOtp(Request $request)
|
||||
{
|
||||
$method = $request->input('method', 'email'); // 'email' atau 'whatsapp'
|
||||
|
||||
if ($method === 'whatsapp') {
|
||||
return $this->sendOtpViaWhatsapp($request);
|
||||
}
|
||||
|
||||
return $this->sendOtpViaEmail($request);
|
||||
}
|
||||
|
||||
// ── Kirim OTP via Email ──
|
||||
private function sendOtpViaEmail(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'email', 'regex:/@gmail\.com$/'],
|
||||
|
|
@ -35,55 +55,103 @@ public function sendOtp(Request $request)
|
|||
]);
|
||||
|
||||
$user = User::where('email', $request->email)->first();
|
||||
if (!$user) {
|
||||
return back()->withErrors(['email' => 'Email tidak ditemukan.'])->withInput();
|
||||
}
|
||||
|
||||
$otp = $this->generateAndStoreOtp($request->email);
|
||||
|
||||
Mail::send('emails.otp', ['otp' => $otp, 'name' => $user->name], function ($m) use ($request) {
|
||||
$m->to($request->email)
|
||||
->subject('Kode Verifikasi Reset Password - Bibit Cabai Bondowoso');
|
||||
});
|
||||
|
||||
$request->session()->put('reset_email', $request->email);
|
||||
$request->session()->put('reset_method', 'email');
|
||||
|
||||
return redirect()->route('password.verify-otp-form')
|
||||
->with('success', 'Kode verifikasi dikirim ke email. Berlaku 3 menit.');
|
||||
}
|
||||
|
||||
// ── Kirim OTP via WhatsApp ──
|
||||
private function sendOtpViaWhatsapp(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'phone' => ['required', 'digits_between:10,15'],
|
||||
], [
|
||||
'phone.required' => 'Nomor WhatsApp wajib diisi.',
|
||||
'phone.digits_between' => 'Nomor WhatsApp tidak valid (10-15 digit).',
|
||||
]);
|
||||
|
||||
$phoneFormatted = FonnteService::formatPhone($request->phone);
|
||||
|
||||
$user = User::where('phone', $phoneFormatted)
|
||||
->orWhere('phone', '+' . $phoneFormatted)
|
||||
->orWhere('phone', $request->phone)
|
||||
->orWhere('phone', '0' . ltrim($request->phone, '0'))
|
||||
->first();
|
||||
|
||||
if (!$user) {
|
||||
return back()->withErrors([
|
||||
'email' => 'Email tidak ditemukan di sistem kami.',
|
||||
'phone' => 'Nomor WhatsApp tidak ditemukan di sistem kami.',
|
||||
])->withInput();
|
||||
}
|
||||
|
||||
// Generate 6-digit OTP
|
||||
// Gunakan email user sebagai key di password_reset_tokens
|
||||
$otp = $this->generateAndStoreOtp($user->email);
|
||||
|
||||
$pesan = "🌶️ *Bibit Cabai Bondowoso*\n\n"
|
||||
. "Halo {$user->name},\n\n"
|
||||
. "Kode verifikasi reset password Anda:\n\n"
|
||||
. "*{$otp}*\n\n"
|
||||
. "Berlaku selama *3 menit*.\n"
|
||||
. "Jangan berikan kode ini kepada siapapun.";
|
||||
|
||||
$sent = $this->fonnte->sendMessage($phoneFormatted, $pesan);
|
||||
|
||||
if (!$sent) {
|
||||
return back()->with('error', 'Gagal mengirim pesan WhatsApp. Coba lagi.');
|
||||
}
|
||||
|
||||
$request->session()->put('reset_email', $user->email);
|
||||
$request->session()->put('reset_method', 'whatsapp');
|
||||
$request->session()->put('reset_phone_display', '****' . substr($request->phone, -4));
|
||||
|
||||
return redirect()->route('password.verify-otp-form')
|
||||
->with('success', 'Kode verifikasi dikirim ke WhatsApp Anda. Berlaku 3 menit.');
|
||||
}
|
||||
|
||||
// ── Helper: generate & simpan OTP ──
|
||||
private function generateAndStoreOtp(string $email): string
|
||||
{
|
||||
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
|
||||
// Delete any existing token for this email
|
||||
DB::table('password_reset_tokens')->where('email', $request->email)->delete();
|
||||
|
||||
// Store hashed OTP + expiry (10 minutes)
|
||||
DB::table('password_reset_tokens')->where('email', $email)->delete();
|
||||
DB::table('password_reset_tokens')->insert([
|
||||
'email' => $request->email,
|
||||
'email' => $email,
|
||||
'token' => Hash::make($otp),
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
// Send OTP via email
|
||||
Mail::send('emails.otp', ['otp' => $otp, 'name' => $user->name], function ($message) use ($request) {
|
||||
$message->to($request->email)
|
||||
->subject('Kode Verifikasi Reset Password - Bibit Cabai Bondowoso');
|
||||
});
|
||||
|
||||
// Store email in session for next steps
|
||||
$request->session()->put('reset_email', $request->email);
|
||||
|
||||
return redirect()->route('password.verify-otp-form')
|
||||
->with('success', 'Kode verifikasi telah dikirim ke email Anda. Berlaku selama 10 menit.');
|
||||
return $otp;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// STEP 2 — Show form: enter OTP
|
||||
// ─────────────────────────────────────────────
|
||||
// ──────────────────────────────────────────────
|
||||
// STEP 2 — Show form: verifikasi OTP
|
||||
// ──────────────────────────────────────────────
|
||||
public function showVerifyOtpForm(Request $request)
|
||||
{
|
||||
if (!$request->session()->has('reset_email')) {
|
||||
return redirect()->route('password.request')
|
||||
->with('error', 'Silakan masukkan email Anda terlebih dahulu.');
|
||||
->with('error', 'Silakan masukkan email atau nomor WA Anda terlebih dahulu.');
|
||||
}
|
||||
|
||||
return view('auth.verify-otp');
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// STEP 2 — Handle: verify OTP
|
||||
// ─────────────────────────────────────────────
|
||||
// ──────────────────────────────────────────────
|
||||
// STEP 2 — Handle: verifikasi OTP
|
||||
// ──────────────────────────────────────────────
|
||||
public function verifyOtp(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
|
|
@ -94,36 +162,30 @@ public function verifyOtp(Request $request)
|
|||
]);
|
||||
|
||||
$email = $request->session()->get('reset_email');
|
||||
|
||||
if (!$email) {
|
||||
return redirect()->route('password.request')
|
||||
->with('error', 'Sesi tidak valid. Silakan ulangi dari awal.');
|
||||
}
|
||||
|
||||
$record = DB::table('password_reset_tokens')
|
||||
->where('email', $email)
|
||||
->first();
|
||||
|
||||
$record = DB::table('password_reset_tokens')->where('email', $email)->first();
|
||||
if (!$record) {
|
||||
return back()->with('error', 'Kode OTP tidak ditemukan. Silakan kirim ulang.');
|
||||
}
|
||||
|
||||
// Check expiry: 10 minutes
|
||||
$createdAt = Carbon::parse($record->created_at);
|
||||
if (Carbon::now()->diffInMinutes($createdAt) > 10) {
|
||||
$diffSeconds = Carbon::now()->diffInSeconds($createdAt);
|
||||
|
||||
if ($diffSeconds > 180) { // 3 menit = 180 detik
|
||||
DB::table('password_reset_tokens')->where('email', $email)->delete();
|
||||
return back()->with('error', 'Kode OTP sudah kadaluarsa. Silakan kirim ulang kode.');
|
||||
return back()->with('error', 'Kode OTP sudah kadaluarsa. Silakan minta kirim kode lagi.');
|
||||
}
|
||||
|
||||
// Verify OTP
|
||||
if (!Hash::check($request->otp, $record->token)) {
|
||||
return back()->with('error', 'Kode OTP salah. Periksa kembali kode yang Anda masukkan.');
|
||||
return back()->with('error', 'Kode OTP salah. Periksa kembali kode Anda.');
|
||||
}
|
||||
|
||||
// Generate a verified token for the next step
|
||||
$verifiedToken = Str::random(60);
|
||||
DB::table('password_reset_tokens')
|
||||
->where('email', $email)
|
||||
DB::table('password_reset_tokens')->where('email', $email)
|
||||
->update(['token' => Hash::make($verifiedToken)]);
|
||||
|
||||
$request->session()->put('reset_token', $verifiedToken);
|
||||
|
|
@ -132,57 +194,62 @@ public function verifyOtp(Request $request)
|
|||
->with('success', 'Kode berhasil diverifikasi! Silakan buat password baru.');
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// STEP 2 — Handle: resend OTP
|
||||
// ─────────────────────────────────────────────
|
||||
// ──────────────────────────────────────────────
|
||||
// STEP 2 — Handle: kirim ulang OTP
|
||||
// ──────────────────────────────────────────────
|
||||
public function resendOtp(Request $request)
|
||||
{
|
||||
$email = $request->session()->get('reset_email');
|
||||
$method = $request->session()->get('reset_method', 'email');
|
||||
|
||||
if (!$email) {
|
||||
return redirect()->route('password.request')
|
||||
->with('error', 'Sesi tidak valid. Silakan masukkan email Anda kembali.');
|
||||
->with('error', 'Sesi tidak valid.');
|
||||
}
|
||||
|
||||
$user = User::where('email', $email)->first();
|
||||
if (!$user) {
|
||||
return redirect()->route('password.request')
|
||||
->with('error', 'Email tidak ditemukan.');
|
||||
return redirect()->route('password.request')->with('error', 'User tidak ditemukan.');
|
||||
}
|
||||
|
||||
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
$otp = $this->generateAndStoreOtp($email);
|
||||
|
||||
DB::table('password_reset_tokens')->where('email', $email)->delete();
|
||||
DB::table('password_reset_tokens')->insert([
|
||||
'email' => $email,
|
||||
'token' => Hash::make($otp),
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
if ($method === 'whatsapp') {
|
||||
$phone = FonnteService::formatPhone(
|
||||
preg_replace('/^\+/', '', $user->phone)
|
||||
);
|
||||
$pesan = "🌶️ *Bibit Cabai Bondowoso*\n\n"
|
||||
. "Halo {$user->name},\n\n"
|
||||
. "Kode verifikasi baru Anda:\n\n"
|
||||
. "*{$otp}*\n\n"
|
||||
. "Berlaku selama *3 menit*.";
|
||||
|
||||
Mail::send('emails.otp', ['otp' => $otp, 'name' => $user->name], function ($message) use ($email) {
|
||||
$message->to($email)
|
||||
->subject('Kode Verifikasi Reset Password (Kirim Ulang) - Bibit Cabai Bondowoso');
|
||||
$sent = $this->fonnte->sendMessage($phone, $pesan);
|
||||
if (!$sent) {
|
||||
return back()->with('error', 'Gagal mengirim ulang ke WhatsApp.');
|
||||
}
|
||||
} else {
|
||||
Mail::send('emails.otp', ['otp' => $otp, 'name' => $user->name], function ($m) use ($email) {
|
||||
$m->to($email)->subject('Kode Verifikasi (Kirim Ulang) - Bibit Cabai Bondowoso');
|
||||
});
|
||||
|
||||
return back()->with('success', 'Kode OTP baru telah dikirim ke email Anda.');
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// STEP 3 — Show form: enter new password
|
||||
// ─────────────────────────────────────────────
|
||||
return back()->with('success', 'Kode OTP baru telah dikirim.');
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// STEP 3 — Show & Handle reset password
|
||||
// (sama seperti kode lama kamu — tidak berubah)
|
||||
// ──────────────────────────────────────────────
|
||||
public function showResetForm(Request $request)
|
||||
{
|
||||
if (!$request->session()->has('reset_token') || !$request->session()->has('reset_email')) {
|
||||
return redirect()->route('password.request')
|
||||
->with('error', 'Sesi tidak valid. Silakan ulangi dari awal.');
|
||||
}
|
||||
|
||||
return view('auth.reset-password');
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// STEP 3 — Handle: save new password
|
||||
// ─────────────────────────────────────────────
|
||||
public function resetPassword(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
|
|
@ -199,27 +266,20 @@ public function resetPassword(Request $request)
|
|||
$token = $request->session()->get('reset_token');
|
||||
|
||||
if (!$email || !$token) {
|
||||
return redirect()->route('password.request')
|
||||
->with('error', 'Sesi tidak valid. Silakan ulangi dari awal.');
|
||||
return redirect()->route('password.request')->with('error', 'Sesi tidak valid.');
|
||||
}
|
||||
|
||||
$record = DB::table('password_reset_tokens')->where('email', $email)->first();
|
||||
|
||||
if (!$record || !Hash::check($token, $record->token)) {
|
||||
return redirect()->route('password.request')
|
||||
->with('error', 'Token tidak valid. Silakan ulangi dari awal.');
|
||||
return redirect()->route('password.request')->with('error', 'Token tidak valid.');
|
||||
}
|
||||
|
||||
// Update password
|
||||
User::where('email', $email)->update([
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
User::where('email', $email)->update(['password' => Hash::make($request->password)]);
|
||||
|
||||
// Clean up
|
||||
DB::table('password_reset_tokens')->where('email', $email)->delete();
|
||||
$request->session()->forget(['reset_email', 'reset_token']);
|
||||
$request->session()->forget(['reset_email', 'reset_token', 'reset_method', 'reset_phone_display']);
|
||||
|
||||
return redirect()->route('login')
|
||||
->with('success', 'Password berhasil diubah! Silakan login dengan password baru Anda.');
|
||||
->with('success', 'Password berhasil diubah! Silakan login.');
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ public function register(Request $request): RedirectResponse
|
|||
'email.regex' => 'Email harus menggunakan domain @gmail.com dan tidak boleh mengandung spasi.',
|
||||
'phone.required' => 'Nomor telepon wajib diisi.',
|
||||
'phone.regex' => 'Nomor telepon harus diawali dengan +62 dan hanya boleh berisi angka.',
|
||||
'phone.unique' => 'Nomor telepon sudah terdaftar di sistem kami.',
|
||||
// 'phone.min' => 'Nomor telepon minimal harus 12 karakter.',
|
||||
'address.required' => 'Alamat wajib diisi.',
|
||||
'address.min' => 'Alamat minimal harus 12 karakter.',
|
||||
|
|
@ -49,9 +50,22 @@ public function register(Request $request): RedirectResponse
|
|||
]);
|
||||
|
||||
// Validasi password familiar
|
||||
// Validasi password familiar & pernah dipakai user lain
|
||||
$validator->after(function ($validator) use ($request) {
|
||||
if ($request->has('password') && $this->isCommonPassword($request->password)) {
|
||||
$validator->errors()->add('password', 'Password yang Anda gunakan terlalu umum dan sering digunakan. Silakan pilih password yang lebih unik untuk keamanan akun Anda.');
|
||||
if ($request->has('password')) {
|
||||
if ($this->isCommonPassword($request->password)) {
|
||||
$validator->errors()->add('password', 'Password yang Anda gunakan terlalu umum. Silakan pilih password yang lebih unik.');
|
||||
}
|
||||
|
||||
if ($this->isPasswordAlreadyUsed($request->password)) {
|
||||
$validator->errors()->add('password', 'Password ini sudah pernah digunakan oleh akun lain. Silakan gunakan password yang berbeda.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->has('phone') && $request->phone) {
|
||||
if ($this->isPhoneAlreadyUsed($request->phone)) {
|
||||
$validator->errors()->add('phone', 'Nomor telepon sudah terdaftar di sistem kami.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -124,4 +138,37 @@ private function isCommonPassword(string $password): bool
|
|||
|
||||
return in_array(strtolower($password), array_map('strtolower', $commonPasswords));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah password sudah pernah dipakai oleh user lain di database
|
||||
*/
|
||||
private function isPasswordAlreadyUsed(string $password): bool
|
||||
{
|
||||
$users = User::whereNotNull('password')->get(['password']);
|
||||
|
||||
foreach ($users as $user) {
|
||||
if (Hash::check($password, $user->password)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek nomor telepon sudah terdaftar dalam berbagai format
|
||||
*/
|
||||
private function isPhoneAlreadyUsed(string $phone): bool
|
||||
{
|
||||
$digitsOnly = preg_replace('/\D/', '', $phone);
|
||||
|
||||
$formats = [
|
||||
$phone, // +6281234567890
|
||||
$digitsOnly, // 6281234567890
|
||||
'+' . $digitsOnly, // +6281234567890
|
||||
'0' . substr($digitsOnly, 2), // 081234567890
|
||||
];
|
||||
|
||||
return User::whereIn('phone', $formats)->exists();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,21 +3,67 @@
|
|||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FonnteService
|
||||
{
|
||||
public function sendOTP($nomor, $otp)
|
||||
{
|
||||
// Format nomor: 08xxx → 628xxx
|
||||
$nomor = '62' . ltrim($nomor, '0');
|
||||
protected string $token;
|
||||
protected string $apiUrl = 'https://api.fonnte.com/send';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->token = config('services.fonnte.token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirim pesan WhatsApp via Fonnte
|
||||
*
|
||||
* @param string $phone Nomor WA tujuan (format: 628xxx)
|
||||
* @param string $message Isi pesan
|
||||
* @return bool
|
||||
*/
|
||||
public function sendMessage(string $phone, string $message): bool
|
||||
{
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => env('FONNTE_TOKEN'),
|
||||
])->post('https://api.fonnte.com/send', [
|
||||
'target' => $nomor,
|
||||
'message' => "Kode OTP reset password Anda: *$otp*\n\nBerlaku selama 10 menit. Jangan berikan kode ini kepada siapapun.",
|
||||
'Authorization' => $this->token,
|
||||
])->post($this->apiUrl, [
|
||||
'target' => $phone,
|
||||
'message' => $message,
|
||||
'countryCode' => '62', // Indonesia
|
||||
]);
|
||||
|
||||
return $response->json();
|
||||
$body = $response->json();
|
||||
|
||||
if ($response->successful() && isset($body['status']) && $body['status'] === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Log::error('Fonnte send failed', ['response' => $body]);
|
||||
return false;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Fonnte exception: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format nomor WA ke format internasional
|
||||
* Misal: 08123 → 628123, +628123 → 628123
|
||||
*/
|
||||
public static function formatPhone(string $phone): string
|
||||
{
|
||||
$phone = preg_replace('/[^\d]/', '', $phone); // hapus +, strip, spasi, dll
|
||||
|
||||
if (str_starts_with($phone, '0')) {
|
||||
$phone = '62' . substr($phone, 1);
|
||||
} elseif (str_starts_with($phone, '62')) {
|
||||
// sudah benar
|
||||
} else {
|
||||
$phone = '62' . $phone;
|
||||
}
|
||||
|
||||
return $phone;
|
||||
}
|
||||
}
|
||||
|
|
@ -35,4 +35,8 @@
|
|||
],
|
||||
],
|
||||
|
||||
'fonnte' => [
|
||||
'token' => env('FONNTE_TOKEN'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@
|
|||
|
||||
<div class="text-center mb-3">
|
||||
<h4 class="fw-bold text-dark mb-1">Lupa Password Admin</h4>
|
||||
<p class="text-muted small">Masukkan email admin yang terdaftar</p>
|
||||
<p class="text-muted small">Gunakan email atau WhatsApp admin terdaftar</p>
|
||||
</div>
|
||||
|
||||
{{-- Step Indicator --}}
|
||||
|
|
@ -136,9 +136,30 @@
|
|||
@endif
|
||||
|
||||
{{-- Form --}}
|
||||
{{-- Toggle Metode --}}
|
||||
<div class="d-flex gap-2 mb-4">
|
||||
<button type="button"
|
||||
class="btn btn-sm flex-fill fw-semibold"
|
||||
id="btnEmail"
|
||||
style="background:#11998e;color:white;border-radius:20px;"
|
||||
onclick="switchMethod('email')">
|
||||
📧 Email
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-sm flex-fill fw-semibold"
|
||||
id="btnWa"
|
||||
style="background:white;color:#11998e;border:2px solid #11998e;border-radius:20px;"
|
||||
onclick="switchMethod('whatsapp')">
|
||||
💬 WhatsApp
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('admin.password.email') }}" id="forgotForm" novalidate>
|
||||
@csrf
|
||||
<input type="hidden" name="method" id="methodInput" value="email">
|
||||
|
||||
{{-- EMAIL SECTION --}}
|
||||
<div id="emailSection">
|
||||
<div class="mb-4">
|
||||
<label for="email" class="form-label fw-semibold">Email Admin</label>
|
||||
<input type="email"
|
||||
|
|
@ -147,8 +168,7 @@ class="form-control @error('email') is-invalid @enderror"
|
|||
name="email"
|
||||
value="{{ old('email') }}"
|
||||
placeholder="admin@example.com"
|
||||
autofocus
|
||||
required>
|
||||
autofocus>
|
||||
<div class="form-text text-muted small">
|
||||
ℹ️ Hanya akun admin yang terdaftar di sistem
|
||||
</div>
|
||||
|
|
@ -159,6 +179,33 @@ class="form-control @error('email') is-invalid @enderror"
|
|||
<small>⚠️ Format email tidak valid</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- WHATSAPP SECTION --}}
|
||||
<div id="waSection" style="display:none;">
|
||||
<div class="mb-4">
|
||||
<label for="phone" class="form-label fw-semibold">Nomor WhatsApp Admin</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text fw-semibold" style="color:#11998e;">+62</span>
|
||||
<input type="tel"
|
||||
class="form-control @error('phone') is-invalid @enderror"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value="{{ old('phone') }}"
|
||||
placeholder="8123456789"
|
||||
inputmode="numeric">
|
||||
</div>
|
||||
<div class="form-text text-muted small">
|
||||
ℹ️ Masukkan nomor tanpa angka 0 di depan
|
||||
</div>
|
||||
@error('phone')
|
||||
<div class="text-danger mt-1 small">{{ $message }}</div>
|
||||
@enderror
|
||||
<div id="phoneError" class="text-danger mt-1" style="display:none;">
|
||||
<small>⚠️ Nomor WhatsApp tidak valid (min 9 digit)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid mb-3">
|
||||
<button type="submit" class="btn btn-primary-custom py-2 fw-bold" id="submitBtn" disabled>
|
||||
|
|
@ -181,32 +228,76 @@ class="form-control @error('email') is-invalid @enderror"
|
|||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const emailInput = document.getElementById('email');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const emailError = document.getElementById('emailError');
|
||||
let currentMethod = 'email';
|
||||
|
||||
function validateEmail(val) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val) && !/\s/.test(val);
|
||||
}
|
||||
function switchMethod(method) {
|
||||
currentMethod = method;
|
||||
document.getElementById('methodInput').value = method;
|
||||
|
||||
emailInput.addEventListener('input', function () {
|
||||
const val = this.value.trim();
|
||||
if (val.length > 0 && !validateEmail(val)) {
|
||||
emailError.style.display = 'block';
|
||||
this.classList.add('is-invalid');
|
||||
submitBtn.disabled = true;
|
||||
const btnEmail = document.getElementById('btnEmail');
|
||||
const btnWa = document.getElementById('btnWa');
|
||||
const emailSec = document.getElementById('emailSection');
|
||||
const waSec = document.getElementById('waSection');
|
||||
|
||||
if (method === 'email') {
|
||||
btnEmail.style.background = '#11998e';
|
||||
btnEmail.style.color = 'white';
|
||||
btnEmail.style.border = 'none';
|
||||
btnWa.style.background = 'white';
|
||||
btnWa.style.color = '#11998e';
|
||||
btnWa.style.border = '2px solid #11998e';
|
||||
emailSec.style.display = 'block';
|
||||
waSec.style.display = 'none';
|
||||
} else {
|
||||
emailError.style.display = 'none';
|
||||
this.classList.remove('is-invalid');
|
||||
submitBtn.disabled = val.length === 0;
|
||||
btnWa.style.background = '#11998e';
|
||||
btnWa.style.color = 'white';
|
||||
btnWa.style.border = 'none';
|
||||
btnEmail.style.background = 'white';
|
||||
btnEmail.style.color = '#11998e';
|
||||
btnEmail.style.border = '2px solid #11998e';
|
||||
emailSec.style.display = 'none';
|
||||
waSec.style.display = 'block';
|
||||
}
|
||||
|
||||
document.getElementById('submitBtn').disabled = true;
|
||||
validateInput();
|
||||
}
|
||||
|
||||
function validateInput() {
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
if (currentMethod === 'email') {
|
||||
const val = document.getElementById('email').value.trim();
|
||||
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val);
|
||||
document.getElementById('emailError').style.display =
|
||||
(val.length > 0 && !valid) ? 'block' : 'none';
|
||||
submitBtn.disabled = !valid;
|
||||
} else {
|
||||
const val = document.getElementById('phone').value.replace(/\D/g, '');
|
||||
const valid = val.length >= 9 && val.length <= 13;
|
||||
document.getElementById('phoneError').style.display =
|
||||
(val.length > 0 && !valid) ? 'block' : 'none';
|
||||
submitBtn.disabled = !valid;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.getElementById('email').addEventListener('input', function () {
|
||||
this.value = this.value.replace(/\s/g, '');
|
||||
validateInput();
|
||||
});
|
||||
|
||||
emailInput.addEventListener('keydown', function (e) {
|
||||
document.getElementById('phone').addEventListener('input', function () {
|
||||
this.value = this.value.replace(/\D/g, '');
|
||||
validateInput();
|
||||
});
|
||||
|
||||
['email', 'phone'].forEach(id => {
|
||||
document.getElementById(id).addEventListener('keydown', function (e) {
|
||||
if (e.key === ' ') e.preventDefault();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
<div class="d-flex align-items-center justify-content-center mb-4">
|
||||
<div class="step-item active">
|
||||
<div class="step-circle bg-success text-white">1</div>
|
||||
<div class="step-label text-success fw-bold">Email</div>
|
||||
<div class="step-label text-success fw-bold">Identitas</div>
|
||||
</div>
|
||||
<div class="step-line bg-secondary mx-2"></div>
|
||||
<div class="step-item">
|
||||
|
|
@ -35,20 +35,32 @@
|
|||
</div>
|
||||
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<div class="alert alert-danger alert-dismissible fade show">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>{{ session('error') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<p class="text-muted text-center mb-4">
|
||||
<i class="fas fa-info-circle text-success me-1"></i>
|
||||
Kami akan mengirim kode verifikasi ke email Anda untuk mereset password.
|
||||
</p>
|
||||
{{-- Toggle Metode --}}
|
||||
<div class="d-flex gap-2 mb-4" id="methodToggle">
|
||||
<button type="button" class="btn btn-success flex-fill" id="btnEmail" onclick="switchMethod('email')">
|
||||
<i class="fas fa-envelope me-1"></i> Gunakan Email
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-success flex-fill" id="btnWa" onclick="switchMethod('whatsapp')">
|
||||
<i class="fab fa-whatsapp me-1"></i> Gunakan WhatsApp
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('password.email') }}" id="forgotForm">
|
||||
@csrf
|
||||
<input type="hidden" name="method" id="methodInput" value="email">
|
||||
|
||||
{{-- Email Section --}}
|
||||
<div id="emailSection">
|
||||
<p class="text-muted text-center mb-3 small">
|
||||
<i class="fas fa-info-circle text-success me-1"></i>
|
||||
Kode verifikasi akan dikirim ke email Anda.
|
||||
</p>
|
||||
<div class="mb-4">
|
||||
<label for="email" class="form-label fw-semibold">
|
||||
Email <span class="text-danger">*</span>
|
||||
|
|
@ -59,11 +71,8 @@
|
|||
</span>
|
||||
<input type="email"
|
||||
class="form-control @error('email') is-invalid @enderror"
|
||||
id="email"
|
||||
name="email"
|
||||
id="email" name="email"
|
||||
value="{{ old('email') }}"
|
||||
required
|
||||
autofocus
|
||||
placeholder="contoh@gmail.com">
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
|
|
@ -76,27 +85,54 @@ class="form-control @error('email') is-invalid @enderror"
|
|||
<small>Email harus menggunakan domain @gmail.com</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- WhatsApp Section --}}
|
||||
<div id="waSection" style="display:none;">
|
||||
<p class="text-muted text-center mb-3 small">
|
||||
<i class="fab fa-whatsapp text-success me-1"></i>
|
||||
Kode verifikasi akan dikirim via WhatsApp.
|
||||
</p>
|
||||
<div class="mb-4">
|
||||
<label for="phone" class="form-label fw-semibold">
|
||||
Nomor WhatsApp <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light fw-semibold text-success">+62</span>
|
||||
<input type="tel"
|
||||
class="form-control @error('phone') is-invalid @enderror"
|
||||
id="phone" name="phone"
|
||||
value="{{ old('phone') }}"
|
||||
placeholder="8123456789"
|
||||
inputmode="numeric">
|
||||
</div>
|
||||
<div class="form-text text-muted">
|
||||
<small><i class="fas fa-info-circle"></i> Masukkan nomor tanpa angka 0 di depan</small>
|
||||
</div>
|
||||
@error('phone')
|
||||
<div class="text-danger mt-1"><small>{{ $message }}</small></div>
|
||||
@enderror
|
||||
<div id="phoneError" class="text-danger mt-1" style="display:none;">
|
||||
<small>Nomor WhatsApp tidak valid</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-success btn-lg" id="submitBtn" disabled>
|
||||
<i class="fas fa-paper-plane me-2"></i>Kirim Kode Verifikasi
|
||||
<i class="fas fa-paper-plane me-2"></i>
|
||||
<span id="submitText">Kirim Kode Verifikasi</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="text-center">
|
||||
<a href="{{ route('login') }}" class="text-success text-decoration-none">
|
||||
<i class="fas fa-arrow-left me-1"></i>Kembali ke Halaman Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.step-item {
|
||||
display: flex;
|
||||
|
|
@ -126,28 +162,57 @@ class="form-control @error('email') is-invalid @enderror"
|
|||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const emailInput = document.getElementById('email');
|
||||
let currentMethod = 'email';
|
||||
|
||||
function switchMethod(method) {
|
||||
currentMethod = method;
|
||||
document.getElementById('methodInput').value = method;
|
||||
|
||||
const btnEmail = document.getElementById('btnEmail');
|
||||
const btnWa = document.getElementById('btnWa');
|
||||
const emailSection = document.getElementById('emailSection');
|
||||
const waSection = document.getElementById('waSection');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const emailError = document.getElementById('emailError');
|
||||
|
||||
emailInput.addEventListener('input', function () {
|
||||
const val = this.value;
|
||||
const gmailPattern = /@gmail\.com$/;
|
||||
const hasSpaces = /\s/.test(val);
|
||||
|
||||
if (val.length > 0 && (!gmailPattern.test(val) || hasSpaces)) {
|
||||
emailError.style.display = 'block';
|
||||
this.classList.add('is-invalid');
|
||||
submitBtn.disabled = true;
|
||||
if (method === 'email') {
|
||||
btnEmail.className = 'btn btn-success flex-fill';
|
||||
btnWa.className = 'btn btn-outline-success flex-fill';
|
||||
emailSection.style.display = 'block';
|
||||
waSection.style.display = 'none';
|
||||
} else {
|
||||
emailError.style.display = 'none';
|
||||
this.classList.remove('is-invalid');
|
||||
submitBtn.disabled = val.length === 0;
|
||||
btnEmail.className = 'btn btn-outline-success flex-fill';
|
||||
btnWa.className = 'btn btn-success flex-fill';
|
||||
emailSection.style.display = 'none';
|
||||
waSection.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
emailInput.addEventListener('keydown', function (e) {
|
||||
submitBtn.disabled = true;
|
||||
validateInput();
|
||||
}
|
||||
|
||||
function validateInput() {
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
if (currentMethod === 'email') {
|
||||
const val = document.getElementById('email').value;
|
||||
const valid = /^[^\s]+@gmail\.com$/.test(val);
|
||||
document.getElementById('emailError').style.display = (val.length > 0 && !valid) ? 'block' : 'none';
|
||||
submitBtn.disabled = !valid;
|
||||
} else {
|
||||
const val = document.getElementById('phone').value.replace(/\D/g, '');
|
||||
const valid = val.length >= 9 && val.length <= 13;
|
||||
document.getElementById('phoneError').style.display = (val.length > 0 && !valid) ? 'block' : 'none';
|
||||
submitBtn.disabled = !valid;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.getElementById('email').addEventListener('input', validateInput);
|
||||
document.getElementById('phone').addEventListener('input', function () {
|
||||
this.value = this.value.replace(/\D/g, '');
|
||||
validateInput();
|
||||
});
|
||||
document.getElementById('phone').addEventListener('keydown', function (e) {
|
||||
if (e.key === ' ') e.preventDefault();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -50,14 +50,22 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Info email target --}}
|
||||
{{-- Info target pengiriman --}}
|
||||
<div class="alert alert-light border-start border-success border-3 mb-4">
|
||||
<div class="d-flex align-items-center">
|
||||
@if(session('reset_method') === 'whatsapp')
|
||||
<i class="fab fa-whatsapp text-success me-3 fs-5"></i>
|
||||
<div>
|
||||
<div class="fw-semibold text-dark">Kode dikirim via WhatsApp ke:</div>
|
||||
<div class="text-muted small">{{ session('reset_phone_display', 'nomor WA Anda') }}</div>
|
||||
</div>
|
||||
@else
|
||||
<i class="fas fa-envelope text-success me-3 fs-5"></i>
|
||||
<div>
|
||||
<div class="fw-semibold text-dark">Kode dikirim ke:</div>
|
||||
<div class="text-muted small">{{ session('reset_email', 'email Anda') }}</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -97,12 +105,13 @@
|
|||
<div class="text-center mb-4">
|
||||
<div id="timerWrapper">
|
||||
<p class="text-muted small mb-1">Kode berlaku selama:</p>
|
||||
<span id="countdown" class="badge bg-success fs-6 px-3 py-2">10:00</span>
|
||||
<span id="countdown" class="badge bg-success fs-6 px-3 py-2">03:00</span>
|
||||
</div>
|
||||
<div id="expiredWrapper" style="display:none;">
|
||||
<span class="badge bg-danger fs-6 px-3 py-2">
|
||||
<i class="fas fa-times-circle me-1"></i>Kode sudah kadaluarsa
|
||||
</span>
|
||||
<div class="alert alert-danger text-center py-2 px-3 mb-0">
|
||||
<i class="fas fa-times-circle me-1"></i>
|
||||
<strong>Kode sudah kadaluarsa.</strong><br>
|
||||
<small>Silakan minta kirim kode lagi.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -190,6 +199,8 @@
|
|||
const timerWrapper = document.getElementById('timerWrapper');
|
||||
const expiredWrapper = document.getElementById('expiredWrapper');
|
||||
|
||||
let isExpired = false; // ← flag global
|
||||
|
||||
// ---- OTP box logic ----
|
||||
boxes.forEach((box, index) => {
|
||||
box.addEventListener('input', function () {
|
||||
|
|
@ -212,7 +223,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Handle paste
|
||||
box.addEventListener('paste', function (e) {
|
||||
e.preventDefault();
|
||||
const pasted = (e.clipboardData || window.clipboardData).getData('text').replace(/\D/g, '');
|
||||
|
|
@ -231,11 +241,12 @@
|
|||
function updateHidden() {
|
||||
const otp = Array.from(boxes).map(b => b.value).join('');
|
||||
otpHidden.value = otp;
|
||||
submitBtn.disabled = otp.length !== 6;
|
||||
// Tombol submit aktif HANYA jika 6 digit DAN belum expired
|
||||
submitBtn.disabled = (otp.length !== 6 || isExpired);
|
||||
}
|
||||
|
||||
// ---- Countdown Timer: 10 minutes ----
|
||||
let seconds = 10 * 60;
|
||||
// ---- Countdown Timer: 3 menit ----
|
||||
let seconds = 3 * 60;
|
||||
|
||||
function formatTime(s) {
|
||||
const m = Math.floor(s / 60).toString().padStart(2, '0');
|
||||
|
|
@ -247,22 +258,38 @@ function formatTime(s) {
|
|||
seconds--;
|
||||
countdownEl.textContent = formatTime(seconds);
|
||||
|
||||
// Berubah merah saat 60 detik terakhir
|
||||
if (seconds <= 60) {
|
||||
countdownEl.classList.replace('bg-success', 'bg-warning');
|
||||
countdownEl.classList.add('text-dark');
|
||||
countdownEl.classList.remove('bg-success', 'bg-warning');
|
||||
countdownEl.classList.add('bg-danger');
|
||||
}
|
||||
|
||||
if (seconds <= 0) {
|
||||
clearInterval(timer);
|
||||
|
||||
// Set flag expired
|
||||
isExpired = true;
|
||||
|
||||
// Sembunyikan timer, tampilkan pesan expired
|
||||
timerWrapper.style.display = 'none';
|
||||
expiredWrapper.style.display = 'block';
|
||||
|
||||
// Nonaktifkan submit & semua kotak OTP
|
||||
submitBtn.disabled = true;
|
||||
// Enable resend after expiry
|
||||
boxes.forEach(box => {
|
||||
box.disabled = true;
|
||||
box.classList.add('bg-light');
|
||||
});
|
||||
|
||||
// Aktifkan tombol kirim ulang
|
||||
resendBtn.disabled = false;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Enable resend after 60s
|
||||
setTimeout(() => { resendBtn.disabled = false; }, 60000);
|
||||
// Aktifkan resend setelah 30 detik
|
||||
setTimeout(() => {
|
||||
if (!isExpired) resendBtn.disabled = false;
|
||||
}, 30000);
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
Loading…
Reference in New Issue