From b76566a2d655b481ece1b314c3fb96c6e1966b76 Mon Sep 17 00:00:00 2001 From: ismailmasseran Date: Mon, 28 Sep 2026 15:05:56 +0800 Subject: [PATCH] Feat/logs password protection (#28) Co-authored-by: ISMAIL MASSERAN Co-authored-by: nurafrinaalimi16 Reviewed-on: https://git.koppkb.com/KoPKB/kaunter/pulls/28 --- .env.example | 2 + .env.production | 1 + app/Http/Controllers/Auth/LoginController.php | 36 ++++++------ app/Http/Middleware/LogViewerPassword.php | 32 +++++++++++ app/Http/Middleware/RoleAccess.php | 6 ++ config/logviewer.php | 1 + routes/web.php | 8 ++- tests/Feature/LogViewerPasswordTest.php | 55 +++++++++++++++++++ 8 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 app/Http/Middleware/LogViewerPassword.php create mode 100644 tests/Feature/LogViewerPasswordTest.php diff --git a/.env.example b/.env.example index 9fd98981..25665a2d 100644 --- a/.env.example +++ b/.env.example @@ -50,3 +50,5 @@ MYKAD_READER_PORT=17321 MYKAD_READER_ENABLED_CAWANGAN='' INTERNAL_NOTIFICATION_SECRET= + +LOGVIEWER_PASSWORD= diff --git a/.env.production b/.env.production index d8af1227..98f9b3b1 100644 --- a/.env.production +++ b/.env.production @@ -7,6 +7,7 @@ APP_URL=https://ujrah.erahn.com.my API_URL=https://apiujrah.erahn.com.my LOG_CHANNEL=stack +LOGVIEWER_PASSWORD="$2y$10$5R7JP/grEqRS6YrAHKKXveuncRoqbbV/IQQLNj3QQDLCyT7g8gL1C" DB_CONNECTION=mysql DB_HOST=192.168.1.216 diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index b9abb660..267c94c9 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -5,15 +5,12 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Providers\RouteServiceProvider; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Session; -use Illuminate\Support\Facades\Redirect; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; use App\Dashboard; use App\User; -use Carbon\Carbon; class LoginController extends Controller { @@ -144,25 +141,26 @@ class LoginController extends Controller } } - public function logout(){ - $current = Carbon::now(); - $current = new Carbon(); - $current = $current->toDateString(); + public function logout(Request $request) + { $auth_user = Auth::guard('web')->user(); - Auth::logout(); - Session::flush(); - $session = Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/logoutsession/' . $auth_user['cawangan'], [ - 'id' => $auth_user['id'], - 'nama' => $auth_user['name'], - 'nokp' => $auth_user['no_kp'], - 'email' => $auth_user['email'], - 'taraf' => $auth_user['role_harian'], - 'tarikh' => $current, - - ]); + Auth::guard('web')->logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); - return Redirect::to('/'); + if ($auth_user) { + Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/logoutsession/' . $auth_user['cawangan'], [ + 'id' => $auth_user['id'], + 'nama' => $auth_user['name'], + 'nokp' => $auth_user['no_kp'], + 'email' => $auth_user['email'], + 'taraf' => $auth_user['role_harian'], + 'tarikh' => now()->toDateString(), + ]); + } + + return redirect('/'); } public function GetRolesHarian(Request $request, $kodcaw){ diff --git a/app/Http/Middleware/LogViewerPassword.php b/app/Http/Middleware/LogViewerPassword.php new file mode 100644 index 00000000..f43d2c40 --- /dev/null +++ b/app/Http/Middleware/LogViewerPassword.php @@ -0,0 +1,32 @@ +header('Cache-Control', 'no-store'); + } + + $provided = $request->getPassword(); + if ($request->getUser() !== 'logviewer' || !is_string($provided) || + !password_verify($provided, $passwordHash)) { + return response('Kata laluan diperlukan untuk mengakses log.', 401, [ + 'WWW-Authenticate' => 'Basic realm="Log Viewer", charset="UTF-8"', + 'Cache-Control' => 'no-store', + ]); + } + + $response = $next($request); + $response->headers->set('Cache-Control', 'no-store'); + + return $response; + } +} diff --git a/app/Http/Middleware/RoleAccess.php b/app/Http/Middleware/RoleAccess.php index 74c33c96..e59b6a87 100644 --- a/app/Http/Middleware/RoleAccess.php +++ b/app/Http/Middleware/RoleAccess.php @@ -44,6 +44,12 @@ class RoleAccess return $next($request); } break; + + case 'masteradmin': + if ($userRole == 'masteradmin') { + return $next($request); + } + break; case 'penilai': if ($userRole == 'penilai') { diff --git a/config/logviewer.php b/config/logviewer.php index 90c2a6f5..5746448d 100644 --- a/config/logviewer.php +++ b/config/logviewer.php @@ -1,6 +1,7 @@ env('LOGVIEWER_PASSWORD'), /* |-------------------------------------------------------------------------- | Pattern and storage path settings diff --git a/routes/web.php b/routes/web.php index 338f3ebf..64bf6c4c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -63,7 +63,13 @@ Route::get('test', function () { return "event test je"; }); -Route::get('logs', '\Rap2hpoutre\LaravelLogViewer\LogViewerController@index'); +Route::get('logs', '\Rap2hpoutre\LaravelLogViewer\LogViewerController@index') + ->middleware([ + 'auth', + 'role:admin,masteradmin,operasi', + 'throttle:10,1', + \App\Http\Middleware\LogViewerPassword::class, + ]); Route::get('change-password', ['as'=>'user.password','uses'=>'ChangePasswordController@index']); Route::post('change-password', ['as'=>'change.password','uses'=>'ChangePasswordController@store']); diff --git a/tests/Feature/LogViewerPasswordTest.php b/tests/Feature/LogViewerPasswordTest.php new file mode 100644 index 00000000..475d85b5 --- /dev/null +++ b/tests/Feature/LogViewerPasswordTest.php @@ -0,0 +1,55 @@ + null]); + $response = $this->checkAccess(); + $this->assertSame(503, $response->getStatusCode()); + } + + public function test_missing_or_incorrect_credentials_are_rejected() + { + config(['logviewer.password' => password_hash('test-only-secret', PASSWORD_DEFAULT)]); + foreach ([[], ['PHP_AUTH_USER' => 'logs', 'PHP_AUTH_PW' => 'wrong'], + ['PHP_AUTH_USER' => 'other', 'PHP_AUTH_PW' => 'test-only-secret']] as $server) { + $response = $this->checkAccess($server); + $this->assertSame(401, $response->getStatusCode()); + $this->assertStringContainsString('Basic realm=', $response->headers->get('WWW-Authenticate')); + } + } + + public function test_correct_credentials_allow_access_without_caching() + { + config(['logviewer.password' => password_hash('test-only-secret', PASSWORD_DEFAULT)]); + $response = $this->checkAccess(['PHP_AUTH_USER' => 'logs', 'PHP_AUTH_PW' => 'test-only-secret']); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('allowed', $response->getContent()); + $this->assertStringContainsString('no-store', $response->headers->get('Cache-Control')); + } + + public function test_logs_route_has_auth_role_password_and_throttle_middleware() + { + $route = app('router')->getRoutes()->match(Request::create('/logs')); + $middleware = $route->gatherMiddleware(); + $this->assertContains('auth', $middleware); + $this->assertContains('role:admin,masteradmin', $middleware); + $this->assertContains(LogViewerPassword::class, $middleware); + $this->assertContains('throttle:10,1', $middleware); + } + + private function checkAccess(array $server = []) + { + $request = Request::create('/logs?dl=example', 'GET', [], [], [], $server); + return (new LogViewerPassword)->handle($request, function () { + return response('allowed'); + }); + } +}