3 Commits

Author SHA1 Message Date
ismailmasseran b76566a2d6 Feat/logs password protection (#28)
Build Docker Image / build-frontend (push) Successful in 42s
Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Co-authored-by: nurafrinaalimi16 <nurafrinaalimi16@gmail.com>
Reviewed-on: #28
2026-09-28 15:05:56 +08:00
ismailmasseran 38f6db2e10 Merge pull request 'fix: papar cara terima pada senarai dan cetakan SAG hanya apabila pembiayaan online aktif' (#27) from fix/sag-cara-terima-feature-flag into main
Reviewed-on: #27
2026-09-28 09:44:25 +08:00
nurafrinaalimi16 f95bdf1214 fix: papar cara terima pada senarai dan cetakan SAG hanya apabila pembiayaan online aktif 2026-09-27 16:55:12 +08:00
12 changed files with 141 additions and 24 deletions
+2
View File
@@ -50,3 +50,5 @@ MYKAD_READER_PORT=17321
MYKAD_READER_ENABLED_CAWANGAN='' MYKAD_READER_ENABLED_CAWANGAN=''
INTERNAL_NOTIFICATION_SECRET= INTERNAL_NOTIFICATION_SECRET=
LOGVIEWER_PASSWORD=
+1
View File
@@ -7,6 +7,7 @@ APP_URL=https://ujrah.erahn.com.my
API_URL=https://apiujrah.erahn.com.my API_URL=https://apiujrah.erahn.com.my
LOG_CHANNEL=stack LOG_CHANNEL=stack
LOGVIEWER_PASSWORD="$2y$10$5R7JP/grEqRS6YrAHKKXveuncRoqbbV/IQQLNj3QQDLCyT7g8gL1C"
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=192.168.1.216 DB_HOST=192.168.1.216
+17 -19
View File
@@ -5,15 +5,12 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider; use App\Providers\RouteServiceProvider;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use App\Dashboard; use App\Dashboard;
use App\User; use App\User;
use Carbon\Carbon;
class LoginController extends Controller class LoginController extends Controller
{ {
@@ -144,25 +141,26 @@ class LoginController extends Controller
} }
} }
public function logout(){ public function logout(Request $request)
$current = Carbon::now(); {
$current = new Carbon();
$current = $current->toDateString();
$auth_user = Auth::guard('web')->user(); $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){ public function GetRolesHarian(Request $request, $kodcaw){
+5 -1
View File
@@ -680,7 +680,11 @@ class GadaianController extends Controller
$kuantiti = $gadai_detail['pinjaman'] / $komoditi['nilai']; $kuantiti = $gadai_detail['pinjaman'] / $komoditi['nilai'];
if ($gadai_detail['tagbaru'] == 1) { if ($gadai_detail['tagbaru'] == 1) {
return view('print.sag_ujrah_3', compact('komoditi', 'product', 'kuantiti', 'gadai_detail', 'customer_detail', 'marhun_detail', 'cawangan_detail', 'api_url', 'pembayaran_segera', 'auth_username', 'mutu', 'kadar_ujrah', 'kakitangan')); $cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/cawangan/' . $kodcaw)->json();
$pembiayaanOnlineEnabled = !empty(($cawangan_info['features'] ?? [])['pembiayaan_online']);
return view('print.sag_ujrah_3', compact('komoditi', 'product', 'kuantiti', 'gadai_detail', 'customer_detail', 'marhun_detail', 'cawangan_detail', 'api_url', 'pembayaran_segera', 'auth_username', 'mutu', 'kadar_ujrah', 'kakitangan'))->with('pembiayaanOnlineEnabled', $pembiayaanOnlineEnabled);
} else { } else {
return view('print.sag', compact('gadai_detail', 'customer_detail', 'marhun_detail', 'cawangan_detail', 'api_url', 'pembayaran_segera', 'auth_username', 'mutu', 'kakitangan')); return view('print.sag', compact('gadai_detail', 'customer_detail', 'marhun_detail', 'cawangan_detail', 'api_url', 'pembayaran_segera', 'auth_username', 'mutu', 'kakitangan'));
} }
@@ -533,6 +533,10 @@ class PembiayaanOnlineController extends Controller
$kuantiti = $nilai > 0 ? ((float) ($gadai_detail['pinjaman'] ?? 0) / $nilai) : 0; $kuantiti = $nilai > 0 ? ((float) ($gadai_detail['pinjaman'] ?? 0) / $nilai) : 0;
if (($gadai_detail['tagbaru'] ?? 0) == 1) { if (($gadai_detail['tagbaru'] ?? 0) == 1) {
$cawangan_info = $this->api()
->get($api_url . '/api/cawangan/' . $kodcaw)->json();
$pembiayaanOnlineEnabled = !empty(($cawangan_info['features'] ?? [])['pembiayaan_online']);
return view('print.sag_ujrah_3', compact( return view('print.sag_ujrah_3', compact(
'komoditi', 'komoditi',
'product', 'product',
@@ -547,7 +551,7 @@ class PembiayaanOnlineController extends Controller
'mutu', 'mutu',
'kadar_ujrah', 'kadar_ujrah',
'kakitangan' 'kakitangan'
)); ))->with('pembiayaanOnlineEnabled', $pembiayaanOnlineEnabled);
} }
return view('print.sag', compact( return view('print.sag', compact(
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Http\Middleware;
use Closure;
class LogViewerPassword
{
public function handle($request, Closure $next)
{
$passwordHash = config('logviewer.password');
if (!is_string($passwordHash) || $passwordHash === '') {
return response('Akses log belum dikonfigurasi.', 503)
->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;
}
}
+6
View File
@@ -44,6 +44,12 @@ class RoleAccess
return $next($request); return $next($request);
} }
break; break;
case 'masteradmin':
if ($userRole == 'masteradmin') {
return $next($request);
}
break;
case 'penilai': case 'penilai':
if ($userRole == 'penilai') { if ($userRole == 'penilai') {
+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
+8 -2
View File
@@ -801,9 +801,12 @@
} }
}); });
var pembiayaanOnlineEnabled = @json(!empty(($cawangan_info['features'] ?? [])['pembiayaan_online']));
var tablegadai = initErahnDatatable('#myTable', { var tablegadai = initErahnDatatable('#myTable', {
order: [[1, 'desc']], order: [[1, 'desc']],
columnDefs: [ columnDefs: [
{ targets: 2, visible: pembiayaanOnlineEnabled, className: pembiayaanOnlineEnabled ? '' : 'no-export' },
{ targets: 0, orderable: false, width: '40px' }, { targets: 0, orderable: false, width: '40px' },
{ targets: -1, orderable: false, searchable: false, width: '70px' } { targets: -1, orderable: false, searchable: false, width: '70px' }
] ]
@@ -811,6 +814,7 @@
var xtawarruq = initErahnDatatable('#xtawarruq', { var xtawarruq = initErahnDatatable('#xtawarruq', {
columnDefs: [ columnDefs: [
{ targets: 2, visible: pembiayaanOnlineEnabled, className: pembiayaanOnlineEnabled ? '' : 'no-export' },
{ targets: 0, orderable: false, width: '40px' } { targets: 0, orderable: false, width: '40px' }
] ]
}); });
@@ -2067,7 +2071,8 @@
harini.setHours(0, 0, 0, 0); harini.setHours(0, 0, 0, 0);
if ($("input[name='options']:checked").val() == 'belum_tebus') { if ($("input[name='options']:checked").val() == 'belum_tebus') {
let test1 = $('input[name="belum"]:checked').closest('tr').children('td:eq(3)').text(); let selectedRow = $('input[name="belum"]:checked').closest('tr');
let test1 = selectedRow.length ? selectedRow.closest('table').DataTable().row(selectedRow).data()[3] : '';
let tarikhgadai2 = new Date(test1); let tarikhgadai2 = new Date(test1);
tarikhgadai2.setHours(0, 0, 0, 0); tarikhgadai2.setHours(0, 0, 0, 0);
if (+harini !== +tarikhgadai2) { if (+harini !== +tarikhgadai2) {
@@ -2484,7 +2489,8 @@
} }
if ($("input[name='options']:checked").val() == 'belum_tebus') { if ($("input[name='options']:checked").val() == 'belum_tebus') {
let test1 = $('input[name="belum"]:checked').closest('tr').children('td:eq(3)').text(); let selectedRow = $('input[name="belum"]:checked').closest('tr');
let test1 = selectedRow.length ? selectedRow.closest('table').DataTable().row(selectedRow).data()[3] : '';
let tarikhgadai2 = new Date(test1); let tarikhgadai2 = new Date(test1);
tarikhgadai2.setHours(0, 0, 0, 0); tarikhgadai2.setHours(0, 0, 0, 0);
@@ -2112,6 +2112,7 @@
</div> </div>
<div class="main-page" style="font-family: Arial, Helvetica, sans-serif;"> <div class="main-page" style="font-family: Arial, Helvetica, sans-serif;">
<div class="sub-page" style="font-size: 11px;"> <div class="sub-page" style="font-size: 11px;">
@if ($pembiayaanOnlineEnabled ?? false)
@php @php
$caraTerimaBayaran = strtoupper(trim((string) ($gadai_detail['cara_terima'] ?? ''))); $caraTerimaBayaran = strtoupper(trim((string) ($gadai_detail['cara_terima'] ?? '')));
@endphp @endphp
@@ -2128,6 +2129,7 @@
</span> </span>
</div> </div>
</div> </div>
@endif
{{-- HEADER SECTION --}} {{-- HEADER SECTION --}}
<table style="width: 100%; margin-top: 0;"> <table style="width: 100%; margin-top: 0;">
<tr style="vertical-align: top;"> <tr style="vertical-align: top;">
+7 -1
View File
@@ -63,7 +63,13 @@ 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([
'auth',
'role:admin,masteradmin,operasi',
'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']);
+55
View File
@@ -0,0 +1,55 @@
<?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' => 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');
});
}
}