feat: protect log viewer with a dedicated password

This commit is contained in:
nurafrinaalimi16
2026-09-28 10:31:49 +08:00
parent 38f6db2e10
commit 076238a25e
5 changed files with 90 additions and 1 deletions
+3
View File
@@ -50,3 +50,6 @@ MYKAD_READER_PORT=17321
MYKAD_READER_ENABLED_CAWANGAN='' MYKAD_READER_ENABLED_CAWANGAN=''
INTERNAL_NOTIFICATION_SECRET= INTERNAL_NOTIFICATION_SECRET=
# Separate password for /logs (username: logs). Empty disables access.
LOGVIEWER_PASSWORD=
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Http\Middleware;
use Closure;
class LogViewerPassword
{
public function handle($request, Closure $next)
{
$password = config('logviewer.password');
if (!is_string($password) || $password === '') {
return response('Akses log belum dikonfigurasi.', 503)
->header('Cache-Control', 'no-store');
}
$provided = $request->getPassword();
if ($request->getUser() !== 'logs' || !is_string($provided) ||
!hash_equals(hash('sha256', $password), hash('sha256', $provided))) {
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;
}
}
+1
View File
@@ -1,6 +1,7 @@
<?php <?php
return [ return [
'password' => env('LOGVIEWER_PASSWORD'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Pattern and storage path settings | Pattern and storage path settings
+2 -1
View File
@@ -63,7 +63,8 @@ Route::get('test', function () {
return "event test je"; return "event test je";
}); });
Route::get('logs', '\Rap2hpoutre\LaravelLogViewer\LogViewerController@index'); Route::get('logs', '\Rap2hpoutre\LaravelLogViewer\LogViewerController@index')
->middleware(['throttle:10,1', \App\Http\Middleware\LogViewerPassword::class]);
Route::get('change-password', ['as'=>'user.password','uses'=>'ChangePasswordController@index']); Route::get('change-password', ['as'=>'user.password','uses'=>'ChangePasswordController@index']);
Route::post('change-password', ['as'=>'change.password','uses'=>'ChangePasswordController@store']); Route::post('change-password', ['as'=>'change.password','uses'=>'ChangePasswordController@store']);
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace Tests\Feature;
use App\Http\Middleware\LogViewerPassword;
use Illuminate\Http\Request;
use Tests\TestCase;
class LogViewerPasswordTest extends TestCase
{
public function test_missing_configuration_denies_access()
{
config(['logviewer.password' => null]);
$response = $this->checkAccess();
$this->assertSame(503, $response->getStatusCode());
}
public function test_missing_or_incorrect_credentials_are_rejected()
{
config(['logviewer.password' => 'test-only-secret']);
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' => 'test-only-secret']);
$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_password_and_throttle_middleware()
{
$route = app('router')->getRoutes()->match(Request::create('/logs'));
$this->assertContains(LogViewerPassword::class, $route->gatherMiddleware());
$this->assertContains('throttle:10,1', $route->gatherMiddleware());
}
private function checkAccess(array $server = [])
{
$request = Request::create('/logs?dl=example', 'GET', [], [], [], $server);
return (new LogViewerPassword)->handle($request, function () {
return response('allowed');
});
}
}