update lupa pw
This commit is contained in:
parent
6ce214315c
commit
aa29baab24
|
|
@ -10,14 +10,24 @@
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use App\Services\FonnteService;
|
||||||
|
|
||||||
class AdminForgotPasswordController extends Controller
|
class AdminForgotPasswordController extends Controller
|
||||||
{
|
{
|
||||||
// ─────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────
|
||||||
// STEP 1 — Tampilkan form input email
|
// STEP 1 — Tampilkan form input email
|
||||||
// ─────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
protected FonnteService $fonnte;
|
||||||
|
|
||||||
|
public function __construct(FonnteService $fonnte)
|
||||||
|
{
|
||||||
|
$this->fonnte = $fonnte;
|
||||||
|
}
|
||||||
|
|
||||||
public function showForgotForm()
|
public function showForgotForm()
|
||||||
{
|
{
|
||||||
|
|
||||||
return view('admin.forgot-password');
|
return view('admin.forgot-password');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,6 +35,17 @@ public function showForgotForm()
|
||||||
// STEP 1 — Proses: validasi email & kirim OTP
|
// STEP 1 — Proses: validasi email & kirim OTP
|
||||||
// ─────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────
|
||||||
public function sendOtp(Request $request)
|
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([
|
$request->validate([
|
||||||
'email' => ['required', 'email'],
|
'email' => ['required', 'email'],
|
||||||
|
|
@ -43,14 +64,7 @@ public function sendOtp(Request $request)
|
||||||
])->withInput();
|
])->withInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
$otp = $this->generateAndStoreOtp($request->email);
|
||||||
|
|
||||||
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(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
Mail::send('emails.admin-otp', [
|
Mail::send('emails.admin-otp', [
|
||||||
'otp' => $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_email', $request->email);
|
||||||
|
$request->session()->put('admin_reset_method', 'email');
|
||||||
|
|
||||||
return redirect()->route('admin.password.verify-otp-form')
|
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)
|
public function resendOtp(Request $request)
|
||||||
{
|
{
|
||||||
$email = $request->session()->get('admin_reset_email');
|
$email = $request->session()->get('admin_reset_email');
|
||||||
|
$method = $request->session()->get('admin_reset_method', 'email');
|
||||||
|
|
||||||
if (!$email) {
|
if (!$email) {
|
||||||
return redirect()->route('admin.password.request')
|
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)
|
$admin = User::where('email', $email)->where('is_admin', true)->first();
|
||||||
->where('is_admin', true)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (!$admin) {
|
if (!$admin) {
|
||||||
return redirect()->route('admin.password.request')
|
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();
|
if ($method === 'whatsapp') {
|
||||||
DB::table('password_reset_tokens')->insert([
|
$phone = FonnteService::formatPhone(
|
||||||
'email' => $email,
|
preg_replace('/^\+/', '', $admin->phone)
|
||||||
'token' => Hash::make($otp),
|
);
|
||||||
'created_at' => Carbon::now(),
|
$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', [
|
Mail::send('emails.admin-otp', [
|
||||||
'otp' => $otp,
|
'otp' => $otp,
|
||||||
'name' => $admin->name ?? 'Admin',
|
'name' => $admin->name ?? 'Admin',
|
||||||
|
|
@ -165,8 +250,9 @@ public function resendOtp(Request $request)
|
||||||
$message->to($email)
|
$message->to($email)
|
||||||
->subject('Kode OTP Baru (Kirim Ulang) - Bibit Cabai Admin');
|
->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\Http\Controllers\Controller;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\FonnteService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
@ -13,18 +14,37 @@
|
||||||
|
|
||||||
class ForgotPasswordController extends Controller
|
class ForgotPasswordController extends Controller
|
||||||
{
|
{
|
||||||
// ─────────────────────────────────────────────
|
protected FonnteService $fonnte;
|
||||||
// STEP 1 — Show form: enter email
|
|
||||||
// ─────────────────────────────────────────────
|
public function __construct(FonnteService $fonnte)
|
||||||
|
{
|
||||||
|
$this->fonnte = $fonnte;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// STEP 1 — Show form: pilih metode (email / WA)
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
public function showForgotForm()
|
public function showForgotForm()
|
||||||
{
|
{
|
||||||
return view('auth.forgot-password');
|
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)
|
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([
|
$request->validate([
|
||||||
'email' => ['required', 'email', 'regex:/@gmail\.com$/'],
|
'email' => ['required', 'email', 'regex:/@gmail\.com$/'],
|
||||||
|
|
@ -35,55 +55,103 @@ public function sendOtp(Request $request)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::where('email', $request->email)->first();
|
$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) {
|
if (!$user) {
|
||||||
return back()->withErrors([
|
return back()->withErrors([
|
||||||
'email' => 'Email tidak ditemukan di sistem kami.',
|
'phone' => 'Nomor WhatsApp tidak ditemukan di sistem kami.',
|
||||||
])->withInput();
|
])->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);
|
$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', $email)->delete();
|
||||||
DB::table('password_reset_tokens')->where('email', $request->email)->delete();
|
|
||||||
|
|
||||||
// Store hashed OTP + expiry (10 minutes)
|
|
||||||
DB::table('password_reset_tokens')->insert([
|
DB::table('password_reset_tokens')->insert([
|
||||||
'email' => $request->email,
|
'email' => $email,
|
||||||
'token' => Hash::make($otp),
|
'token' => Hash::make($otp),
|
||||||
'created_at' => Carbon::now(),
|
'created_at' => Carbon::now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Send OTP via email
|
return $otp;
|
||||||
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.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
// STEP 2 — Show form: enter OTP
|
// STEP 2 — Show form: verifikasi OTP
|
||||||
// ─────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
public function showVerifyOtpForm(Request $request)
|
public function showVerifyOtpForm(Request $request)
|
||||||
{
|
{
|
||||||
if (!$request->session()->has('reset_email')) {
|
if (!$request->session()->has('reset_email')) {
|
||||||
return redirect()->route('password.request')
|
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');
|
return view('auth.verify-otp');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
// STEP 2 — Handle: verify OTP
|
// STEP 2 — Handle: verifikasi OTP
|
||||||
// ─────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
public function verifyOtp(Request $request)
|
public function verifyOtp(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
|
|
@ -94,36 +162,30 @@ public function verifyOtp(Request $request)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$email = $request->session()->get('reset_email');
|
$email = $request->session()->get('reset_email');
|
||||||
|
|
||||||
if (!$email) {
|
if (!$email) {
|
||||||
return redirect()->route('password.request')
|
return redirect()->route('password.request')
|
||||||
->with('error', 'Sesi tidak valid. Silakan ulangi dari awal.');
|
->with('error', 'Sesi tidak valid. Silakan ulangi dari awal.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$record = DB::table('password_reset_tokens')
|
$record = DB::table('password_reset_tokens')->where('email', $email)->first();
|
||||||
->where('email', $email)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (!$record) {
|
if (!$record) {
|
||||||
return back()->with('error', 'Kode OTP tidak ditemukan. Silakan kirim ulang.');
|
return back()->with('error', 'Kode OTP tidak ditemukan. Silakan kirim ulang.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check expiry: 10 minutes
|
|
||||||
$createdAt = Carbon::parse($record->created_at);
|
$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();
|
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)) {
|
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);
|
$verifiedToken = Str::random(60);
|
||||||
DB::table('password_reset_tokens')
|
DB::table('password_reset_tokens')->where('email', $email)
|
||||||
->where('email', $email)
|
|
||||||
->update(['token' => Hash::make($verifiedToken)]);
|
->update(['token' => Hash::make($verifiedToken)]);
|
||||||
|
|
||||||
$request->session()->put('reset_token', $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.');
|
->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)
|
public function resendOtp(Request $request)
|
||||||
{
|
{
|
||||||
$email = $request->session()->get('reset_email');
|
$email = $request->session()->get('reset_email');
|
||||||
|
$method = $request->session()->get('reset_method', 'email');
|
||||||
|
|
||||||
if (!$email) {
|
if (!$email) {
|
||||||
return redirect()->route('password.request')
|
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();
|
$user = User::where('email', $email)->first();
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
return redirect()->route('password.request')
|
return redirect()->route('password.request')->with('error', 'User tidak ditemukan.');
|
||||||
->with('error', 'Email 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();
|
if ($method === 'whatsapp') {
|
||||||
DB::table('password_reset_tokens')->insert([
|
$phone = FonnteService::formatPhone(
|
||||||
'email' => $email,
|
preg_replace('/^\+/', '', $user->phone)
|
||||||
'token' => Hash::make($otp),
|
);
|
||||||
'created_at' => Carbon::now(),
|
$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) {
|
$sent = $this->fonnte->sendMessage($phone, $pesan);
|
||||||
$message->to($email)
|
if (!$sent) {
|
||||||
->subject('Kode Verifikasi Reset Password (Kirim Ulang) - Bibit Cabai Bondowoso');
|
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.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────
|
return back()->with('success', 'Kode OTP baru telah dikirim.');
|
||||||
// STEP 3 — Show form: enter new password
|
}
|
||||||
// ─────────────────────────────────────────────
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// STEP 3 — Show & Handle reset password
|
||||||
|
// (sama seperti kode lama kamu — tidak berubah)
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
public function showResetForm(Request $request)
|
public function showResetForm(Request $request)
|
||||||
{
|
{
|
||||||
if (!$request->session()->has('reset_token') || !$request->session()->has('reset_email')) {
|
if (!$request->session()->has('reset_token') || !$request->session()->has('reset_email')) {
|
||||||
return redirect()->route('password.request')
|
return redirect()->route('password.request')
|
||||||
->with('error', 'Sesi tidak valid. Silakan ulangi dari awal.');
|
->with('error', 'Sesi tidak valid. Silakan ulangi dari awal.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('auth.reset-password');
|
return view('auth.reset-password');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────
|
|
||||||
// STEP 3 — Handle: save new password
|
|
||||||
// ─────────────────────────────────────────────
|
|
||||||
public function resetPassword(Request $request)
|
public function resetPassword(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
|
|
@ -199,27 +266,20 @@ public function resetPassword(Request $request)
|
||||||
$token = $request->session()->get('reset_token');
|
$token = $request->session()->get('reset_token');
|
||||||
|
|
||||||
if (!$email || !$token) {
|
if (!$email || !$token) {
|
||||||
return redirect()->route('password.request')
|
return redirect()->route('password.request')->with('error', 'Sesi tidak valid.');
|
||||||
->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 || !Hash::check($token, $record->token)) {
|
if (!$record || !Hash::check($token, $record->token)) {
|
||||||
return redirect()->route('password.request')
|
return redirect()->route('password.request')->with('error', 'Token tidak valid.');
|
||||||
->with('error', 'Token tidak valid. Silakan ulangi dari awal.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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();
|
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')
|
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.',
|
'email.regex' => 'Email harus menggunakan domain @gmail.com dan tidak boleh mengandung spasi.',
|
||||||
'phone.required' => 'Nomor telepon wajib diisi.',
|
'phone.required' => 'Nomor telepon wajib diisi.',
|
||||||
'phone.regex' => 'Nomor telepon harus diawali dengan +62 dan hanya boleh berisi angka.',
|
'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.',
|
// 'phone.min' => 'Nomor telepon minimal harus 12 karakter.',
|
||||||
'address.required' => 'Alamat wajib diisi.',
|
'address.required' => 'Alamat wajib diisi.',
|
||||||
'address.min' => 'Alamat minimal harus 12 karakter.',
|
'address.min' => 'Alamat minimal harus 12 karakter.',
|
||||||
|
|
@ -49,9 +50,22 @@ public function register(Request $request): RedirectResponse
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Validasi password familiar
|
// Validasi password familiar
|
||||||
|
// Validasi password familiar & pernah dipakai user lain
|
||||||
$validator->after(function ($validator) use ($request) {
|
$validator->after(function ($validator) use ($request) {
|
||||||
if ($request->has('password') && $this->isCommonPassword($request->password)) {
|
if ($request->has('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 ($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));
|
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;
|
namespace App\Services;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class FonnteService
|
class FonnteService
|
||||||
{
|
{
|
||||||
public function sendOTP($nomor, $otp)
|
protected string $token;
|
||||||
{
|
protected string $apiUrl = 'https://api.fonnte.com/send';
|
||||||
// Format nomor: 08xxx → 628xxx
|
|
||||||
$nomor = '62' . ltrim($nomor, '0');
|
|
||||||
|
|
||||||
|
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([
|
$response = Http::withHeaders([
|
||||||
'Authorization' => env('FONNTE_TOKEN'),
|
'Authorization' => $this->token,
|
||||||
])->post('https://api.fonnte.com/send', [
|
])->post($this->apiUrl, [
|
||||||
'target' => $nomor,
|
'target' => $phone,
|
||||||
'message' => "Kode OTP reset password Anda: *$otp*\n\nBerlaku selama 10 menit. Jangan berikan kode ini kepada siapapun.",
|
'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">
|
<div class="text-center mb-3">
|
||||||
<h4 class="fw-bold text-dark mb-1">Lupa Password Admin</h4>
|
<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>
|
</div>
|
||||||
|
|
||||||
{{-- Step Indicator --}}
|
{{-- Step Indicator --}}
|
||||||
|
|
@ -136,9 +136,30 @@
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- Form --}}
|
{{-- 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>
|
<form method="POST" action="{{ route('admin.password.email') }}" id="forgotForm" novalidate>
|
||||||
@csrf
|
@csrf
|
||||||
|
<input type="hidden" name="method" id="methodInput" value="email">
|
||||||
|
|
||||||
|
{{-- EMAIL SECTION --}}
|
||||||
|
<div id="emailSection">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="email" class="form-label fw-semibold">Email Admin</label>
|
<label for="email" class="form-label fw-semibold">Email Admin</label>
|
||||||
<input type="email"
|
<input type="email"
|
||||||
|
|
@ -147,8 +168,7 @@ class="form-control @error('email') is-invalid @enderror"
|
||||||
name="email"
|
name="email"
|
||||||
value="{{ old('email') }}"
|
value="{{ old('email') }}"
|
||||||
placeholder="admin@example.com"
|
placeholder="admin@example.com"
|
||||||
autofocus
|
autofocus>
|
||||||
required>
|
|
||||||
<div class="form-text text-muted small">
|
<div class="form-text text-muted small">
|
||||||
ℹ️ Hanya akun admin yang terdaftar di sistem
|
ℹ️ Hanya akun admin yang terdaftar di sistem
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -159,6 +179,33 @@ class="form-control @error('email') is-invalid @enderror"
|
||||||
<small>⚠️ Format email tidak valid</small>
|
<small>⚠️ Format email tidak valid</small>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="d-grid mb-3">
|
||||||
<button type="submit" class="btn btn-primary-custom py-2 fw-bold" id="submitBtn" disabled>
|
<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 src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
let currentMethod = 'email';
|
||||||
const emailInput = document.getElementById('email');
|
|
||||||
const submitBtn = document.getElementById('submitBtn');
|
|
||||||
const emailError = document.getElementById('emailError');
|
|
||||||
|
|
||||||
function validateEmail(val) {
|
function switchMethod(method) {
|
||||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val) && !/\s/.test(val);
|
currentMethod = method;
|
||||||
}
|
document.getElementById('methodInput').value = method;
|
||||||
|
|
||||||
emailInput.addEventListener('input', function () {
|
const btnEmail = document.getElementById('btnEmail');
|
||||||
const val = this.value.trim();
|
const btnWa = document.getElementById('btnWa');
|
||||||
if (val.length > 0 && !validateEmail(val)) {
|
const emailSec = document.getElementById('emailSection');
|
||||||
emailError.style.display = 'block';
|
const waSec = document.getElementById('waSection');
|
||||||
this.classList.add('is-invalid');
|
|
||||||
submitBtn.disabled = true;
|
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 {
|
} else {
|
||||||
emailError.style.display = 'none';
|
btnWa.style.background = '#11998e';
|
||||||
this.classList.remove('is-invalid');
|
btnWa.style.color = 'white';
|
||||||
submitBtn.disabled = val.length === 0;
|
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();
|
if (e.key === ' ') e.preventDefault();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
<div class="d-flex align-items-center justify-content-center mb-4">
|
<div class="d-flex align-items-center justify-content-center mb-4">
|
||||||
<div class="step-item active">
|
<div class="step-item active">
|
||||||
<div class="step-circle bg-success text-white">1</div>
|
<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>
|
||||||
<div class="step-line bg-secondary mx-2"></div>
|
<div class="step-line bg-secondary mx-2"></div>
|
||||||
<div class="step-item">
|
<div class="step-item">
|
||||||
|
|
@ -35,20 +35,32 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(session('error'))
|
@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') }}
|
<i class="fas fa-exclamation-circle me-2"></i>{{ session('error') }}
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<p class="text-muted text-center mb-4">
|
{{-- Toggle Metode --}}
|
||||||
<i class="fas fa-info-circle text-success me-1"></i>
|
<div class="d-flex gap-2 mb-4" id="methodToggle">
|
||||||
Kami akan mengirim kode verifikasi ke email Anda untuk mereset password.
|
<button type="button" class="btn btn-success flex-fill" id="btnEmail" onclick="switchMethod('email')">
|
||||||
</p>
|
<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">
|
<form method="POST" action="{{ route('password.email') }}" id="forgotForm">
|
||||||
@csrf
|
@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">
|
<div class="mb-4">
|
||||||
<label for="email" class="form-label fw-semibold">
|
<label for="email" class="form-label fw-semibold">
|
||||||
Email <span class="text-danger">*</span>
|
Email <span class="text-danger">*</span>
|
||||||
|
|
@ -59,11 +71,8 @@
|
||||||
</span>
|
</span>
|
||||||
<input type="email"
|
<input type="email"
|
||||||
class="form-control @error('email') is-invalid @enderror"
|
class="form-control @error('email') is-invalid @enderror"
|
||||||
id="email"
|
id="email" name="email"
|
||||||
name="email"
|
|
||||||
value="{{ old('email') }}"
|
value="{{ old('email') }}"
|
||||||
required
|
|
||||||
autofocus
|
|
||||||
placeholder="contoh@gmail.com">
|
placeholder="contoh@gmail.com">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-text text-muted">
|
<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>
|
<small>Email harus menggunakan domain @gmail.com</small>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="d-grid">
|
||||||
<button type="submit" class="btn btn-success btn-lg" id="submitBtn" disabled>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a href="{{ route('login') }}" class="text-success text-decoration-none">
|
<a href="{{ route('login') }}" class="text-success text-decoration-none">
|
||||||
<i class="fas fa-arrow-left me-1"></i>Kembali ke Halaman Login
|
<i class="fas fa-arrow-left me-1"></i>Kembali ke Halaman Login
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.step-item {
|
.step-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -126,28 +162,57 @@ class="form-control @error('email') is-invalid @enderror"
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
let currentMethod = 'email';
|
||||||
const emailInput = document.getElementById('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 submitBtn = document.getElementById('submitBtn');
|
||||||
const emailError = document.getElementById('emailError');
|
|
||||||
|
|
||||||
emailInput.addEventListener('input', function () {
|
if (method === 'email') {
|
||||||
const val = this.value;
|
btnEmail.className = 'btn btn-success flex-fill';
|
||||||
const gmailPattern = /@gmail\.com$/;
|
btnWa.className = 'btn btn-outline-success flex-fill';
|
||||||
const hasSpaces = /\s/.test(val);
|
emailSection.style.display = 'block';
|
||||||
|
waSection.style.display = 'none';
|
||||||
if (val.length > 0 && (!gmailPattern.test(val) || hasSpaces)) {
|
|
||||||
emailError.style.display = 'block';
|
|
||||||
this.classList.add('is-invalid');
|
|
||||||
submitBtn.disabled = true;
|
|
||||||
} else {
|
} else {
|
||||||
emailError.style.display = 'none';
|
btnEmail.className = 'btn btn-outline-success flex-fill';
|
||||||
this.classList.remove('is-invalid');
|
btnWa.className = 'btn btn-success flex-fill';
|
||||||
submitBtn.disabled = val.length === 0;
|
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();
|
if (e.key === ' ') e.preventDefault();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -50,14 +50,22 @@
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- Info email target --}}
|
{{-- Info target pengiriman --}}
|
||||||
<div class="alert alert-light border-start border-success border-3 mb-4">
|
<div class="alert alert-light border-start border-success border-3 mb-4">
|
||||||
<div class="d-flex align-items-center">
|
<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>
|
<i class="fas fa-envelope text-success me-3 fs-5"></i>
|
||||||
<div>
|
<div>
|
||||||
<div class="fw-semibold text-dark">Kode dikirim ke:</div>
|
<div class="fw-semibold text-dark">Kode dikirim ke:</div>
|
||||||
<div class="text-muted small">{{ session('reset_email', 'email Anda') }}</div>
|
<div class="text-muted small">{{ session('reset_email', 'email Anda') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -97,12 +105,13 @@
|
||||||
<div class="text-center mb-4">
|
<div class="text-center mb-4">
|
||||||
<div id="timerWrapper">
|
<div id="timerWrapper">
|
||||||
<p class="text-muted small mb-1">Kode berlaku selama:</p>
|
<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>
|
||||||
<div id="expiredWrapper" style="display:none;">
|
<div id="expiredWrapper" style="display:none;">
|
||||||
<span class="badge bg-danger fs-6 px-3 py-2">
|
<div class="alert alert-danger text-center py-2 px-3 mb-0">
|
||||||
<i class="fas fa-times-circle me-1"></i>Kode sudah kadaluarsa
|
<i class="fas fa-times-circle me-1"></i>
|
||||||
</span>
|
<strong>Kode sudah kadaluarsa.</strong><br>
|
||||||
|
<small>Silakan minta kirim kode lagi.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -190,6 +199,8 @@
|
||||||
const timerWrapper = document.getElementById('timerWrapper');
|
const timerWrapper = document.getElementById('timerWrapper');
|
||||||
const expiredWrapper = document.getElementById('expiredWrapper');
|
const expiredWrapper = document.getElementById('expiredWrapper');
|
||||||
|
|
||||||
|
let isExpired = false; // ← flag global
|
||||||
|
|
||||||
// ---- OTP box logic ----
|
// ---- OTP box logic ----
|
||||||
boxes.forEach((box, index) => {
|
boxes.forEach((box, index) => {
|
||||||
box.addEventListener('input', function () {
|
box.addEventListener('input', function () {
|
||||||
|
|
@ -212,7 +223,6 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle paste
|
|
||||||
box.addEventListener('paste', function (e) {
|
box.addEventListener('paste', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const pasted = (e.clipboardData || window.clipboardData).getData('text').replace(/\D/g, '');
|
const pasted = (e.clipboardData || window.clipboardData).getData('text').replace(/\D/g, '');
|
||||||
|
|
@ -231,11 +241,12 @@
|
||||||
function updateHidden() {
|
function updateHidden() {
|
||||||
const otp = Array.from(boxes).map(b => b.value).join('');
|
const otp = Array.from(boxes).map(b => b.value).join('');
|
||||||
otpHidden.value = otp;
|
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 ----
|
// ---- Countdown Timer: 3 menit ----
|
||||||
let seconds = 10 * 60;
|
let seconds = 3 * 60;
|
||||||
|
|
||||||
function formatTime(s) {
|
function formatTime(s) {
|
||||||
const m = Math.floor(s / 60).toString().padStart(2, '0');
|
const m = Math.floor(s / 60).toString().padStart(2, '0');
|
||||||
|
|
@ -247,22 +258,38 @@ function formatTime(s) {
|
||||||
seconds--;
|
seconds--;
|
||||||
countdownEl.textContent = formatTime(seconds);
|
countdownEl.textContent = formatTime(seconds);
|
||||||
|
|
||||||
|
// Berubah merah saat 60 detik terakhir
|
||||||
if (seconds <= 60) {
|
if (seconds <= 60) {
|
||||||
countdownEl.classList.replace('bg-success', 'bg-warning');
|
countdownEl.classList.remove('bg-success', 'bg-warning');
|
||||||
countdownEl.classList.add('text-dark');
|
countdownEl.classList.add('bg-danger');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seconds <= 0) {
|
if (seconds <= 0) {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
|
|
||||||
|
// Set flag expired
|
||||||
|
isExpired = true;
|
||||||
|
|
||||||
|
// Sembunyikan timer, tampilkan pesan expired
|
||||||
timerWrapper.style.display = 'none';
|
timerWrapper.style.display = 'none';
|
||||||
expiredWrapper.style.display = 'block';
|
expiredWrapper.style.display = 'block';
|
||||||
|
|
||||||
|
// Nonaktifkan submit & semua kotak OTP
|
||||||
submitBtn.disabled = true;
|
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;
|
resendBtn.disabled = false;
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
// Enable resend after 60s
|
// Aktifkan resend setelah 30 detik
|
||||||
setTimeout(() => { resendBtn.disabled = false; }, 60000);
|
setTimeout(() => {
|
||||||
|
if (!isExpired) resendBtn.disabled = false;
|
||||||
|
}, 30000);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
Loading…
Reference in New Issue