From 64f3c29e2133c8fbd7785b60fcaddc9e95793b27 Mon Sep 17 00:00:00 2001 From: JokoPrasetio Date: Wed, 13 May 2026 09:25:21 +0700 Subject: [PATCH] fixing add captcha --- app/Http/Controllers/AuthController.php | 97 ++++++++++++++++++++++++- resources/views/auth/index.blade.php | 16 ++++ routes/web.php | 1 + 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 88e5f03..7b710c9 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -12,8 +12,10 @@ class AuthController extends Controller private int $maxLoginAttempts = 10; public function index(){ + $captcha = $this->generateCaptchaCode(6); + session(['login_captcha' => $captcha]); $data = [ - 'title' => 'Login Admin | Order Gizi' + 'title' => 'Login Admin | Order Gizi', ]; return view('auth.index', $data); } @@ -22,6 +24,7 @@ class AuthController extends Controller $validated = $request->validate([ 'username' => 'required', 'password' => 'required', + 'captcha' => 'required', 'website' => 'nullable', ]); @@ -32,6 +35,30 @@ class AuthController extends Controller $now = time(); $rateKey = 'login:' . $request->ip() . ':' . strtolower((string) $request->input('username')); + $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('username')) + ->with(['alertError' => 'rate']); + } + + $backoffKey = $rateKey . ':backoff_until'; + $until = (int) $request->session()->get($backoffKey, 0); + if($until > $now){ + return back()->withInput($request->only('username')) + ->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('username')) + ->with(['alertError' => 'captcha']); + } + $request->session()->forget('login_captcha'); + // IMPORTANT: only pass auth credentials to Auth::attempt // (do not include captcha / honeypot fields, otherwise Laravel will query non-existent columns) @@ -42,9 +69,18 @@ class AuthController extends Controller if(Auth::attempt($credentials)){ $request->session()->regenerate(); + $request->session()->forget($rateKey); + $request->session()->forget($backoffKey); return redirect()->intended('/dashboard'); } + // 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!']); } @@ -55,4 +91,63 @@ class AuthController extends Controller request()->session()->regenerateToken(); return redirect('/login'); } + + public function captcha(){ + $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); + + 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 + ); + } + + for($i = 0; $i < 180; $i++){ + imagesetpixel($img, random_int(0, $width - 1), random_int(0, $height - 1), $noise); + } + + $font = 5; + $textWidth = imagefontwidth($font) * strlen($captcha); + $textHeight = imagefontwidth($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 generateCaptchaCode(int $length = 6): string + { + $chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; + $out = ''; + $max = strlen($chars) - 1; + for ($i = 0; $i < $length; $i++){ + $out .= $chars[random_int(0, $max)]; + } + return $out; + } } diff --git a/resources/views/auth/index.blade.php b/resources/views/auth/index.blade.php index a100a89..3273613 100644 --- a/resources/views/auth/index.blade.php +++ b/resources/views/auth/index.blade.php @@ -64,6 +64,8 @@ Terlalu banyak percobaan login. Coba lagi dalam 1 menit. @elseif(session('alertError') === 'backoff') Mohon tunggu beberapa detik sebelum mencoba lagi. + @elseif(session('alertError') === 'captcha') + Captcha tidak sesuai @else Username atau password salah! @endif @@ -98,6 +100,20 @@ +
+ +
+ captcha + + Refresh +
+
Masukkan kode sesuai yang ditampilkan (huruf tidak membedakan kapital/kecil).
+
diff --git a/routes/web.php b/routes/web.php index 6c5dea5..3ee69c7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -22,6 +22,7 @@ use Illuminate\Support\Facades\Route; // }); Route::get('/login', [AuthController::class, 'index'])->name('login')->middleware('guest'); Route::post('/login', [AuthController::class, 'authanticate'])->middleware('throttle:login'); +Route::get('/captcha/login', [AuthController::class, 'captcha'])->name('captcha.login'); Route::group(['middleware' => ['auth']], function(){ Route::group(['prefix' => 'dashboard'], function(){