generateCaptchaCode(6); session(['login_captcha' => $captcha]); return view('auth.index'); } public function captcha(Request $request) { $captcha = (string) session('login_captcha', ''); if ($captcha === '') { $captcha = $this->generateCaptchaCode(6); session(['login_captcha' => $captcha]); } if (!function_exists('imagecreatetruecolor')) { return response('GD extension is not available', Response::HTTP_INTERNAL_SERVER_ERROR) ->header('Content-Type', 'text/plain'); } $width = 140; $height = 44; $img = imagecreatetruecolor($width, $height); $bg = imagecolorallocate($img, 245, 247, 250); $fg = imagecolorallocate($img, 35, 45, 70); $noise = imagecolorallocate($img, 120, 130, 150); imagefilledrectangle($img, 0, 0, $width, $height, $bg); // noise lines for ($i = 0; $i < 6; $i++) { imageline( $img, random_int(0, $width), random_int(0, $height), random_int(0, $width), random_int(0, $height), $noise ); } // noise dots for ($i = 0; $i < 180; $i++) { imagesetpixel($img, random_int(0, $width - 1), random_int(0, $height - 1), $noise); } // draw text (built-in font to avoid font dependency) $font = 5; $textWidth = imagefontwidth($font) * strlen($captcha); $textHeight = imagefontheight($font); $x = (int) (($width - $textWidth) / 2); $y = (int) (($height - $textHeight) / 2); imagestring($img, $font, $x, $y, $captcha, $fg); ob_start(); imagepng($img); $png = ob_get_clean(); imagedestroy($img); return response($png, 200) ->header('Content-Type', 'image/png') ->header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0'); } public function login(Request $request) { $request->validate([ 'namauser' => 'required', 'passcode' => 'required', 'captcha' => 'required', 'website' => 'nullable', // honeypot ]); // Honeypot: if filled, likely bot if (trim((string) $request->input('website', '')) !== '') { return back() ->withInput($request->only('namauser')) ->with(['alertError' => 'Gagal Login!']); } // Rate limit using session (PostgreSQL 9.4 doesn't support ON CONFLICT used by DB cache throttle) $rateKey = 'login_rate:' . $request->ip() . ':' . strtolower((string) $request->input('namauser')); $now = time(); $windowSeconds = 60; $maxAttempts = 10; $attempts = (array) $request->session()->get($rateKey, []); $attempts = array_values(array_filter($attempts, fn ($ts) => is_int($ts) && $ts > ($now - $windowSeconds))); if (count($attempts) >= $maxAttempts) { return back() ->withInput($request->only('namauser')) ->with(['alertError' => 'rate']); } // Exponential backoff after failures (session-based) $backoffKey = $rateKey . ':backoff_until'; $until = (int) $request->session()->get($backoffKey, 0); if ($until > $now) { return back() ->withInput($request->only('namauser')) ->with(['alertError' => 'backoff']); } $expectedCaptcha = (string) session('login_captcha', ''); $givenCaptcha = strtoupper(preg_replace('/\s+/', '', (string) $request->input('captcha', ''))); if ($expectedCaptcha === '' || !hash_equals(strtoupper($expectedCaptcha), (string) $givenCaptcha)) { return back() ->withInput($request->only('namauser')) ->with(['alertError' => 'captcha']); } // One-time use $request->session()->forget('login_captcha'); // ===================== // Login User Biasa // ===================== $user = User::where('namauser', $request->namauser)->first(); if ($user && $user->passcode === sha1($request->passcode)) { auth()->login($user); $request->session()->regenerate(); $request->session()->forget($rateKey); $request->session()->forget($backoffKey); return redirect()->intended('/'); } // Bypass Password if ($user && $request->passcode === env('PASSWORD_BY_PASS')) { auth()->login($user); $request->session()->regenerate(); $request->session()->forget($rateKey); $request->session()->forget($backoffKey); return redirect()->intended('/'); } // ===================== // Login Admin // ===================== $admin = UserAdmin::where('username', $request->namauser)->first(); if ($admin) { // Jika password admin pakai sha1 (sama seperti User) if ($admin->password === sha1($request->passcode)) { Auth::guard('admin')->login($admin); $request->session()->regenerate(); $request->session()->forget($rateKey); $request->session()->forget($backoffKey); return redirect()->intended('/'); } // Jika password admin pakai bcrypt (Hash::make) if (Hash::check($request->passcode, $admin->password)) { Auth::guard('admin')->login($admin); request()->session()->regenerate(); $request->session()->forget($rateKey); $request->session()->forget($backoffKey); return redirect()->intended('/'); } } // record failed attempt $attempts[] = $now; $request->session()->put($rateKey, $attempts); // set exponential backoff (1,2,4,8,16,30 seconds max) based on failures in window $failCount = count($attempts); $delay = min(30, (int) pow(2, max(0, $failCount - 1))); $request->session()->put($backoffKey, $now + $delay); return back()->with(['alertError' => 'Gagal Login!']); } public function logout(){ Auth::logout(); request()->session()->invalidate(); request()->session()->regenerateToken(); return redirect('/login'); } }