diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index b6318f3..6546825 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -4,26 +4,108 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Http\Response; +use Illuminate\Support\Facades\RateLimiter; class AuthController extends Controller { + private int $captchaTtlSeconds = 120; + private int $loginDecaySeconds = 60; + private int $maxLoginAttempts = 10; + + private function generateCaptchaCode(int $length = 6): string + { + // Avoid ambiguous chars: 0,O,1,I,l + $chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; + $out = ''; + $max = strlen($chars) - 1; + for ($i = 0; $i < $length; $i++) { + $out .= $chars[random_int(0, $max)]; + } + return $out; + } + + private function refreshCaptcha(): string + { + $captcha = $this->generateCaptchaCode(6); + session([ + 'login_captcha' => $captcha, + 'login_captcha_created_at' => now()->getTimestamp(), + ]); + + return $captcha; + } + + private function ensureCaptchaValid(): void + { + $createdAt = (int) session('login_captcha_created_at', 0); + $expired = $createdAt <= 0 || (now()->getTimestamp() - $createdAt) > $this->captchaTtlSeconds; + + if ($expired || (string) session('login_captcha', '') === '') { + $this->refreshCaptcha(); + } + } + public function index(){ + $this->refreshCaptcha(); $data = [ 'title' => 'Login Admin | Order Gizi' ]; return view('auth.index', $data); } - public function authanticate(){ - $credentials = request()->validate([ + public function authanticate(Request $request){ + $validated = $request->validate([ 'username' => 'required', - 'password' => 'required' + 'password' => 'required', + 'captcha' => 'required', + 'website' => 'nullable', ]); + + if (trim((string) $request->input('website', '')) !== '') { + return back()->withInput($request->only('username')) + ->with(['alertError' => 'Gagal Login!']); + } + + $now = time(); + $rateKey = 'login:' . $request->ip() . ':' . strtolower((string) $request->input('username')); + if (RateLimiter::tooManyAttempts($rateKey, $this->maxLoginAttempts)) { + return back() + ->withInput($request->only('username')) + ->with(['alertError' => 'rate']); + } + + $this->ensureCaptchaValid(); + $expectedCaptcha = (string) session('login_captcha', ''); + $givenCaptcha = strtoupper(preg_replace('/\s+/', '', (string) $request->input('captcha', ''))); + if ($expectedCaptcha === '' || !hash_equals(strtoupper($expectedCaptcha), (string) $givenCaptcha)) { + RateLimiter::hit($rateKey, $this->loginDecaySeconds); + $this->refreshCaptcha(); + return back() + ->withInput($request->only('username')) + ->with(['alertError' => 'captcha']); + } + // One-time use + $request->session()->forget('login_captcha'); + $request->session()->forget('login_captcha_created_at'); + + // IMPORTANT: only pass auth credentials to Auth::attempt + // (do not include captcha / honeypot fields, otherwise Laravel will query non-existent columns) + $credentials = [ + 'username' => (string) ($validated['username'] ?? ''), + 'password' => (string) ($validated['password'] ?? ''), + ]; + if(Auth::attempt($credentials)){ - request()->session()->regenerate(); + $request->session()->regenerate(); + RateLimiter::clear($rateKey); return redirect()->intended('/dashboard'); } - return back()->with(['alertError' => 'Terdapat kesalahan disini!']); + + RateLimiter::hit($rateKey, $this->loginDecaySeconds); + $this->refreshCaptcha(); + + return back()->with(['alertError' => 'Gagal Login!']); } public function logout() @@ -33,4 +115,58 @@ class AuthController extends Controller request()->session()->regenerateToken(); return redirect('/login'); } + + public function captcha(Request $request){ + $this->ensureCaptchaValid(); + $captcha = (string) session('login_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'); + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 4cfb6ef..e25e262 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,7 +2,10 @@ namespace App\Providers; +use Illuminate\Cache\RateLimiting\Limit; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Blade; +use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; @@ -21,6 +24,11 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { + RateLimiter::for('login', function (Request $request) { + // Coarse protection against floods; detailed failure-based limiting stays in AuthController. + return Limit::perMinute(60)->by($request->ip()); + }); + Blade::component('dashboard.pesanan.components.modalExport', 'modalExport'); Blade::component('dashboard.pesanan.components.infoPesanan', 'infoPesanan'); if ($this->app->environment('production')) { diff --git a/resources/views/auth/index.blade.php b/resources/views/auth/index.blade.php index 38e51ac..7f1133d 100644 --- a/resources/views/auth/index.blade.php +++ b/resources/views/auth/index.blade.php @@ -58,11 +58,19 @@
Please sign-in to your account
- @if (session()->has('alertError')) -