project_directory/app/Http/Controllers/AuthController.php
2026-05-05 11:26:06 +07:00

204 lines
7.2 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\LogActivity;
use App\Models\MappingUnitKerjaPegawai;
use App\Models\User;
use App\Models\UserAdmin;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
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;
}
public function index(){
// Simple numeric captcha (no external service)
$captcha = $this->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');
}
}