Fix WASA security observations: security headers, CSP hardening, JS library updates, API authentication, and data scrubbing
This commit is contained in:
parent
8cbf55537f
commit
c548c0dce3
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Config;
|
||||
|
||||
use App\Filters\ApiAuthFilter;
|
||||
use App\Filters\SecureHeadersFilter;
|
||||
use App\Filters\SingleActiveSessionFilter;
|
||||
use CodeIgniter\Config\Filters as BaseFilters;
|
||||
|
|
@ -37,6 +38,7 @@ class Filters extends BaseFilters
|
|||
'pagecache' => PageCache::class,
|
||||
'performance' => PerformanceMetrics::class,
|
||||
'singleSession' => SingleActiveSessionFilter::class,
|
||||
'apiAuth' => ApiAuthFilter::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1676,7 +1676,7 @@ $routes->match(['GET', 'HEAD'], 'app/queue/manifest.json', 'Api\v1\DoctorApi::qu
|
|||
$routes->match(['GET', 'HEAD'], 'app/queue/(:any)', 'Api\v1\DoctorApi::queueIndex');
|
||||
|
||||
// Dedicated RESTful JSON API v1 Group for React PWAs & Mobile Apps
|
||||
$routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static function ($routes) {
|
||||
$routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1', 'filter' => 'apiAuth'], static function ($routes) {
|
||||
// Live TV Queue Display API
|
||||
$routes->get('opd/queue/live', 'DoctorApi::liveQueueDisplay');
|
||||
// Nursing PWA APIs
|
||||
|
|
|
|||
|
|
@ -49,18 +49,25 @@ class DoctorApi extends BaseController
|
|||
|
||||
/**
|
||||
* GET api/v1/doctor/list
|
||||
* Returns sanitized doctor list for UI pickers (Data Minimized: PII and signatures stripped)
|
||||
* Returns sanitized doctor list for authorized users only (All Sensitive/Auth Fields Scrubbed)
|
||||
*/
|
||||
public function doctors()
|
||||
{
|
||||
if (! $this->isAuthenticatedDoctorOrUser()) {
|
||||
return $this->response->setStatusCode(401)->setJSON([
|
||||
'status' => 0,
|
||||
'message' => 'Unauthorized access. Authentication is required to view doctor directory.',
|
||||
]);
|
||||
}
|
||||
|
||||
$doctors = $this->doctorModel->getDoctors();
|
||||
$list = [];
|
||||
foreach ($doctors as $d) {
|
||||
$name = trim(($d->p_title ?? '') . ' ' . ($d->p_fname ?? '') . ' ' . ($d->p_lname ?? ''));
|
||||
$list[] = [
|
||||
'id' => (int) $d->id,
|
||||
'name' => trim(($d->p_title ?? '') . ' ' . ($d->p_fname ?? '')),
|
||||
'name' => $name,
|
||||
'department' => $d->department ?? '',
|
||||
'has_pin' => ! empty($d->app_pin),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +77,61 @@ class DoctorApi extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to verify if the requesting client has an active authenticated session or valid token
|
||||
*/
|
||||
protected function isAuthenticatedDoctorOrUser(): bool
|
||||
{
|
||||
if (function_exists('auth') && auth()->loggedIn()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$session = session();
|
||||
if ($session->has('user_id') && (int) $session->get('user_id') > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($session->has('pwa_auth_doctor_id') && (int) $session->get('pwa_auth_doctor_id') > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($session->has('pwa_auth_nurse_id') && (int) $session->get('pwa_auth_nurse_id') > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$token = trim((string) $this->request->getHeaderLine('X-Device-Token'));
|
||||
if ($token === '') {
|
||||
$authHeader = trim((string) $this->request->getHeaderLine('Authorization'));
|
||||
if (preg_match('/Bearer\s+([a-f0-9]{64})/i', $authHeader, $matches)) {
|
||||
$token = $matches[1];
|
||||
}
|
||||
}
|
||||
if ($token === '') {
|
||||
$token = trim((string) ($this->request->getGet('device_token') ?? $this->request->getPost('device_token') ?? ''));
|
||||
}
|
||||
|
||||
if ($token !== '' && strlen($token) === 64) {
|
||||
if (service('cache')->get('api_device_token_' . $token)) {
|
||||
return true;
|
||||
}
|
||||
$doctors = db_connect()->table('doctor_master')
|
||||
->select('id, device_code')
|
||||
->where('active', 1)
|
||||
->where('device_code IS NOT NULL')
|
||||
->where('device_code !=', '')
|
||||
->get()->getResultArray();
|
||||
foreach ($doctors as $doc) {
|
||||
$expected = hash_hmac('sha256', (string) $doc['id'], (string) $doc['device_code']);
|
||||
if (hash_equals($expected, $token)) {
|
||||
service('cache')->save('api_device_token_' . $token, ['role' => 'doctor', 'id' => (int) $doc['id']], 86400 * 30);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isHospitalIntranet(string $ip): bool
|
||||
{
|
||||
if (in_array($ip, ['127.0.0.1', '::1', 'localhost'], true)) {
|
||||
|
|
@ -248,6 +310,7 @@ class DoctorApi extends BaseController
|
|||
if (session_status() === PHP_SESSION_ACTIVE || ! session_id()) {
|
||||
session()->set('pwa_auth_doctor_id', (int) $doctorRow->id);
|
||||
}
|
||||
service('cache')->save('api_device_token_' . $expectedDeviceToken, ['role' => 'doctor', 'id' => (int) $doctorRow->id], 86400 * 30);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
|
|
|
|||
|
|
@ -68,6 +68,13 @@ class NursingApi extends BaseController
|
|||
*/
|
||||
public function beds()
|
||||
{
|
||||
if (! $this->isAuthenticatedNurseOrUser()) {
|
||||
return $this->response->setStatusCode(401)->setJSON([
|
||||
'status' => 0,
|
||||
'message' => 'Unauthorized access. Authentication is required to view hospital bed records.',
|
||||
]);
|
||||
}
|
||||
|
||||
$db = db_connect();
|
||||
$sql = "SELECT b.id, b.bed_number, b.bed_code, b.bed_category_id, b.ward_id, b.current_ipd_id,
|
||||
b.status, coalesce(b.bed_status, 'available') as bed_status,
|
||||
|
|
@ -106,10 +113,17 @@ class NursingApi extends BaseController
|
|||
|
||||
/**
|
||||
* GET api/v1/nursing/nurses
|
||||
* Returns sanitized active nurse staff list for PWA staff switcher (Data Minimized & Credentials Stripped)
|
||||
* Returns sanitized active nurse staff list for authorized users only (All Sensitive/Auth Fields Scrubbed)
|
||||
*/
|
||||
public function nurses()
|
||||
{
|
||||
if (! $this->isAuthenticatedNurseOrUser()) {
|
||||
return $this->response->setStatusCode(401)->setJSON([
|
||||
'status' => 0,
|
||||
'message' => 'Unauthorized access. Authentication is required to view nursing staff data.',
|
||||
]);
|
||||
}
|
||||
|
||||
$nurses = $this->nurseModel->getActiveNurses();
|
||||
$safeList = [];
|
||||
foreach ($nurses as $n) {
|
||||
|
|
@ -117,11 +131,8 @@ class NursingApi extends BaseController
|
|||
'id' => (int) ($n['id'] ?? 0),
|
||||
'nurse_code' => (string) ($n['nurse_code'] ?? ''),
|
||||
'name' => (string) ($n['name'] ?? ''),
|
||||
'gender' => (string) ($n['gender'] ?? ''),
|
||||
'designation' => (string) ($n['designation'] ?? 'Staff Nurse'),
|
||||
'department' => (string) ($n['department'] ?? 'Nursing'),
|
||||
'has_pin' => ! empty($n['app_pin']),
|
||||
'is_active' => (int) ($n['is_active'] ?? 1),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -131,6 +142,61 @@ class NursingApi extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to verify if the requesting client has an active authenticated session or valid token
|
||||
*/
|
||||
protected function isAuthenticatedNurseOrUser(): bool
|
||||
{
|
||||
if (function_exists('auth') && auth()->loggedIn()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$session = session();
|
||||
if ($session->has('user_id') && (int) $session->get('user_id') > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($session->has('pwa_auth_nurse_id') && (int) $session->get('pwa_auth_nurse_id') > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($session->has('pwa_auth_doctor_id') && (int) $session->get('pwa_auth_doctor_id') > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$token = trim((string) $this->request->getHeaderLine('X-Device-Token'));
|
||||
if ($token === '') {
|
||||
$authHeader = trim((string) $this->request->getHeaderLine('Authorization'));
|
||||
if (preg_match('/Bearer\s+([a-f0-9]{64})/i', $authHeader, $matches)) {
|
||||
$token = $matches[1];
|
||||
}
|
||||
}
|
||||
if ($token === '') {
|
||||
$token = trim((string) ($this->request->getGet('device_token') ?? $this->request->getPost('device_token') ?? ''));
|
||||
}
|
||||
|
||||
if ($token !== '' && strlen($token) === 64) {
|
||||
if (service('cache')->get('api_device_token_' . $token)) {
|
||||
return true;
|
||||
}
|
||||
$nurses = db_connect()->table('nurse_master')
|
||||
->select('id, nurse_code, device_code')
|
||||
->where('is_active', 1)
|
||||
->where('device_code IS NOT NULL')
|
||||
->where('device_code !=', '')
|
||||
->get()->getResultArray();
|
||||
foreach ($nurses as $nurse) {
|
||||
$expected = hash_hmac('sha256', $nurse['id'] . '_' . ($nurse['nurse_code'] ?? ''), (string) $nurse['device_code']);
|
||||
if (hash_equals($expected, $token)) {
|
||||
service('cache')->save('api_device_token_' . $token, ['role' => 'nurse', 'id' => (int) $nurse['id']], 86400 * 30);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isHospitalIntranet(string $ip): bool
|
||||
{
|
||||
if (in_array($ip, ['127.0.0.1', '::1', 'localhost'], true)) {
|
||||
|
|
@ -300,6 +366,7 @@ class NursingApi extends BaseController
|
|||
if (session_status() === PHP_SESSION_ACTIVE || ! session_id()) {
|
||||
session()->set('pwa_auth_nurse_id', (int) $nurseRow['id']);
|
||||
}
|
||||
service('cache')->save('api_device_token_' . $expectedDeviceToken, ['role' => 'nurse', 'id' => (int) $nurseRow['id']], 86400 * 30);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
|
|
|
|||
148
app/Filters/ApiAuthFilter.php
Normal file
148
app/Filters/ApiAuthFilter.php
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filters;
|
||||
|
||||
use CodeIgniter\Filters\FilterInterface;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Enforces Server-Side Authentication & Authorization for API v1 Endpoints (WASA CWE-306 Remediation)
|
||||
*/
|
||||
class ApiAuthFilter implements FilterInterface
|
||||
{
|
||||
/**
|
||||
* Endpoints that do not require prior authentication (e.g. login/verify-pin, live TV queue display)
|
||||
*/
|
||||
protected array $exemptPaths = [
|
||||
'api/v1/nursing/auth/verify-pin',
|
||||
'api/v1/nursing/auth/set-pin',
|
||||
'api/v1/doctor/auth/verify-pin',
|
||||
'api/v1/doctor/auth/set-pin',
|
||||
'api/v1/opd/queue/live',
|
||||
];
|
||||
|
||||
public function before(RequestInterface $request, $arguments = null)
|
||||
{
|
||||
// Allow CORS preflight OPTIONS requests
|
||||
if (strtoupper($request->getMethod()) === 'OPTIONS') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$uriPath = trim((string) ($request->getUri()->getPath() ?? ''), '/');
|
||||
|
||||
// Check if path is explicitly exempt
|
||||
foreach ($this->exemptPaths as $exempt) {
|
||||
if ($uriPath === $exempt || str_ends_with($uriPath, $exempt)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Check active user session (CodeIgniter Shield / HMS Admin)
|
||||
if (function_exists('auth') && auth()->loggedIn()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$session = session();
|
||||
if ($session->has('user_id') && (int) $session->get('user_id') > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. Check active PWA role-based sessions
|
||||
if ($session->has('pwa_auth_nurse_id') && (int) $session->get('pwa_auth_nurse_id') > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($session->has('pwa_auth_doctor_id') && (int) $session->get('pwa_auth_doctor_id') > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($session->has('pwa_auth_store_id') && (int) $session->get('pwa_auth_store_id') > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. Check token from header or request parameters
|
||||
$token = trim((string) $request->getHeaderLine('X-Device-Token'));
|
||||
if ($token === '') {
|
||||
$authHeader = trim((string) $request->getHeaderLine('Authorization'));
|
||||
if (preg_match('/Bearer\s+([a-f0-9]{64})/i', $authHeader, $matches)) {
|
||||
$token = $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
if ($token === '') {
|
||||
$token = trim((string) ($request->getGet('device_token') ?? $request->getPost('device_token') ?? ''));
|
||||
}
|
||||
|
||||
if ($token !== '' && strlen($token) === 64) {
|
||||
if ($this->validateDeviceToken($token)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Authentication failed - return 401 Unauthorized
|
||||
return service('response')
|
||||
->setStatusCode(401)
|
||||
->setJSON([
|
||||
'status' => 0,
|
||||
'error' => 'Unauthorized',
|
||||
'message' => 'Authentication required. Access to this endpoint requires an active session or valid authorization token.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||
{
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates device token against cache or database
|
||||
*/
|
||||
protected function validateDeviceToken(string $token): bool
|
||||
{
|
||||
$cache = service('cache');
|
||||
$cacheKey = 'api_device_token_' . $token;
|
||||
|
||||
if ($cache->get($cacheKey)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
// Check against active nurses
|
||||
$nurses = $db->table('nurse_master')
|
||||
->select('id, nurse_code, device_code')
|
||||
->where('is_active', 1)
|
||||
->where('device_code IS NOT NULL')
|
||||
->where('device_code !=', '')
|
||||
->get()->getResultArray();
|
||||
|
||||
foreach ($nurses as $nurse) {
|
||||
$expected = hash_hmac('sha256', $nurse['id'] . '_' . ($nurse['nurse_code'] ?? ''), (string) $nurse['device_code']);
|
||||
if (hash_equals($expected, $token)) {
|
||||
$cache->save($cacheKey, ['role' => 'nurse', 'id' => (int) $nurse['id']], 86400 * 30);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check against active doctors
|
||||
$doctors = $db->table('doctor_master')
|
||||
->select('id, device_code')
|
||||
->where('active', 1)
|
||||
->where('device_code IS NOT NULL')
|
||||
->where('device_code !=', '')
|
||||
->get()->getResultArray();
|
||||
|
||||
foreach ($doctors as $doctor) {
|
||||
$expected = hash_hmac('sha256', (string) $doctor['id'], (string) $doctor['device_code']);
|
||||
if (hash_equals($expected, $token)) {
|
||||
$cache->save($cacheKey, ['role' => 'doctor', 'id' => (int) $doctor['id']], 86400 * 30);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -43,10 +43,12 @@ class SecureHeadersFilter implements FilterInterface
|
|||
|
||||
// Cross-Origin isolation protections
|
||||
'Cross-Origin-Opener-Policy' => 'same-origin-allow-popups',
|
||||
'Cross-Origin-Resource-Policy' => 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy' => 'credentialless',
|
||||
|
||||
// Content Security Policy (strict sources, trusted CDNs, preventing wildcards and injection)
|
||||
'Content-Security-Policy' => "default-src 'self'; " .
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https://cdn.jsdelivr.net https://code.jquery.com https://cdn.datatables.net https://oss.maxcdn.com https://cdnjs.cloudflare.com; " .
|
||||
"script-src 'self' 'unsafe-inline' data: blob: https://cdn.jsdelivr.net https://code.jquery.com https://cdn.datatables.net https://cdnjs.cloudflare.com; " .
|
||||
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdn.datatables.net https://fonts.googleapis.com https://cdnjs.cloudflare.com; " .
|
||||
"font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com; " .
|
||||
"img-src 'self' data: blob: https://api.qrserver.com; " .
|
||||
|
|
@ -68,13 +70,29 @@ class SecureHeadersFilter implements FilterInterface
|
|||
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||
{
|
||||
$uriPath = trim((string) ($request->getUri()->getPath() ?? ''), '/');
|
||||
$isApi = str_starts_with($uriPath, 'api/') || str_contains((string) $response->getHeaderLine('Content-Type'), 'application/json');
|
||||
|
||||
foreach ($this->headers as $header => $value) {
|
||||
// Apply strict sandbox CSP on API responses
|
||||
if ($header === 'Content-Security-Policy' && $isApi) {
|
||||
$response->setHeader('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'; sandbox;");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only set if not already explicitly defined by a specific controller
|
||||
if (! $response->hasHeader($header)) {
|
||||
$response->setHeader($header, $value);
|
||||
}
|
||||
}
|
||||
|
||||
// Sensitive API endpoints must never be stored in browser/proxy caches
|
||||
if ($isApi) {
|
||||
$response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
||||
$response->setHeader('Pragma', 'no-cache');
|
||||
$response->setHeader('Expires', '0');
|
||||
}
|
||||
|
||||
// Provide fresh CSRF token header for AJAX/fetch calls
|
||||
if (function_exists('csrf_hash')) {
|
||||
$response->setHeader('X-CSRF-TOKEN', csrf_hash());
|
||||
|
|
|
|||
|
|
@ -19,12 +19,6 @@
|
|||
|
||||
<link rel="stylesheet" href="<?php echo base_url('assets/dist/css/AdminLTE.min.css'); ?>">
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
|
||||
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body onload="window.print();">
|
||||
<div class="wrapper">
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Create New Packing</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
|
||||
<style>
|
||||
.card-header {
|
||||
|
|
@ -74,8 +74,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#createForm').on('submit', function(e) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Edit Packing</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
|
||||
|
|
@ -122,8 +122,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script>
|
||||
const packingId = <?= $packing_id ?>;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Organization Packing Management</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
|
||||
<style>
|
||||
.card-header {
|
||||
|
|
@ -69,8 +69,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Auto-search on page load
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>IPD Packing List - <?= esc($packing->label_no) ?></title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
@media print {
|
||||
.no-print { display: none; }
|
||||
|
|
@ -132,6 +132,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>OPD Packing List - <?= esc($packing->label_no) ?></title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
@media print {
|
||||
.no-print { display: none; }
|
||||
|
|
@ -128,6 +128,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@
|
|||
<div class="toast-container position-fixed top-0 end-0 p-3" id="toast-container"></div>
|
||||
<a href="#" class="back-to-top d-flex align-items-center justify-content-center"><i class="bi bi-arrow-up-short"></i></a>
|
||||
|
||||
<script src="<?= base_url('assets/vendor/jquery/jquery-4.0.0.min.js') ?>"></script>
|
||||
<script src="<?= base_url('assets/vendor/jquery/jquery-3.7.1.min.js') ?>"></script>
|
||||
<script src="<?= base_url('assets/js/csrf-guard.js') ?>"></script>
|
||||
<script src="<?= base_url('assets/vendor/inputmask/inputmask.min.js') ?>"></script>
|
||||
<script src="<?= base_url('assets/vendor/inputmask/jquery.inputmask.min.js') ?>"></script>
|
||||
|
|
|
|||
2392
public/App/Doctor/doctor-app.bundle.js
Normal file
2392
public/App/Doctor/doctor-app.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
20275
public/App/MedicalStore/medicalstore-app.bundle.js
Normal file
20275
public/App/MedicalStore/medicalstore-app.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
4540
public/App/Nursing/nursing-app.bundle.js
Normal file
4540
public/App/Nursing/nursing-app.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
2
public/assets/vendor/jquery/jquery-3.7.1.min.js
vendored
Normal file
2
public/assets/vendor/jquery/jquery-3.7.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
public/assets/vendor/select2/select2.min.css
vendored
2
public/assets/vendor/select2/select2.min.css
vendored
File diff suppressed because one or more lines are too long
5
public/assets/vendor/select2/select2.min.js
vendored
5
public/assets/vendor/select2/select2.min.js
vendored
File diff suppressed because one or more lines are too long
76
scripts/build_pwas.js
Normal file
76
scripts/build_pwas.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
const babelPath = path.join(rootDir, 'public', 'assets', 'vendor', 'react', 'babel.min.js');
|
||||
|
||||
console.log('Loading Babel standalone from:', babelPath);
|
||||
const babelCode = fs.readFileSync(babelPath, 'utf8');
|
||||
|
||||
const context = {
|
||||
window: {},
|
||||
self: {},
|
||||
console: console,
|
||||
Array,
|
||||
Object,
|
||||
Math,
|
||||
Date,
|
||||
RegExp,
|
||||
String,
|
||||
Number,
|
||||
Boolean,
|
||||
Error,
|
||||
parseFloat,
|
||||
parseInt,
|
||||
isNaN,
|
||||
isFinite
|
||||
};
|
||||
context.globalThis = context;
|
||||
vm.createContext(context);
|
||||
vm.runInContext(babelCode, context);
|
||||
const Babel = context.Babel || context.window.Babel;
|
||||
|
||||
if (!Babel) {
|
||||
console.error('Failed to initialize Babel');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Babel standalone initialized successfully. Version:', Babel.version);
|
||||
|
||||
const apps = [
|
||||
{ name: 'Doctor', dir: path.join(rootDir, 'public', 'App', 'Doctor'), bundleName: 'doctor-app.bundle.js' },
|
||||
{ name: 'Nursing', dir: path.join(rootDir, 'public', 'App', 'Nursing'), bundleName: 'nursing-app.bundle.js' },
|
||||
{ name: 'MedicalStore', dir: path.join(rootDir, 'public', 'App', 'MedicalStore'), bundleName: 'medicalstore-app.bundle.js' }
|
||||
];
|
||||
|
||||
for (const app of apps) {
|
||||
const htmlPath = path.join(app.dir, 'index.html');
|
||||
if (!fs.existsSync(htmlPath)) {
|
||||
console.warn(`HTML file not found: ${htmlPath}`);
|
||||
continue;
|
||||
}
|
||||
console.log(`\n--- Processing ${app.name} ---`);
|
||||
const htmlContent = fs.readFileSync(htmlPath, 'utf8');
|
||||
const match = htmlContent.match(/<script type="text\/babel">([\s\S]*?)<\/script>/);
|
||||
if (!match) {
|
||||
console.warn(`No <script type="text/babel"> found in ${app.name}/index.html`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const jsxSource = match[1];
|
||||
console.log(`Found JSX code in ${app.name}, size: ${(jsxSource.length / 1024).toFixed(2)} KB`);
|
||||
|
||||
console.log(`Compiling JSX with Babel presets: ['react']...`);
|
||||
const startTime = Date.now();
|
||||
const result = Babel.transform(jsxSource, {
|
||||
presets: ['react'],
|
||||
compact: false
|
||||
});
|
||||
console.log(`Compilation finished in ${Date.now() - startTime}ms. Output JS size: ${(result.code.length / 1024).toFixed(2)} KB`);
|
||||
|
||||
const bundlePath = path.join(app.dir, app.bundleName);
|
||||
fs.writeFileSync(bundlePath, result.code, 'utf8');
|
||||
console.log(`Saved bundle to: ${bundlePath}`);
|
||||
}
|
||||
|
||||
console.log('\nAll PWA apps precompiled successfully!');
|
||||
41
scripts/switch_to_bundles.js
Normal file
41
scripts/switch_to_bundles.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
|
||||
const apps = [
|
||||
{ name: 'Doctor', dir: path.join(rootDir, 'public', 'App', 'Doctor'), bundleName: 'doctor-app.bundle.js' },
|
||||
{ name: 'Nursing', dir: path.join(rootDir, 'public', 'App', 'Nursing'), bundleName: 'nursing-app.bundle.js' },
|
||||
{ name: 'MedicalStore', dir: path.join(rootDir, 'public', 'App', 'MedicalStore'), bundleName: 'medicalstore-app.bundle.js' }
|
||||
];
|
||||
|
||||
for (const app of apps) {
|
||||
const htmlPath = path.join(app.dir, 'index.html');
|
||||
let content = fs.readFileSync(htmlPath, 'utf8');
|
||||
|
||||
// 1. Remove unsafe-eval from meta CSP
|
||||
content = content.replace(/'unsafe-eval'\s*/g, '');
|
||||
|
||||
// 2. Remove babel.min.js script tag
|
||||
content = content.replace(/\s*<script src="\/assets\/vendor\/react\/babel\.min\.js"><\/script>/g, '');
|
||||
|
||||
// 3. Replace <script type="text/babel">...</script> with the bundle script
|
||||
const babelStart = content.indexOf('<script type="text/babel">');
|
||||
if (babelStart !== -1) {
|
||||
const babelEnd = content.indexOf('</script>', babelStart);
|
||||
if (babelEnd !== -1) {
|
||||
const before = content.substring(0, babelStart);
|
||||
const after = content.substring(babelEnd + '</script>'.length);
|
||||
content = before + `<script src="/App/${app.name}/${app.bundleName}"></script>` + after;
|
||||
console.log(`Updated ${app.name}/index.html to use ${app.bundleName}`);
|
||||
} else {
|
||||
console.warn(`Could not find closing </script> for ${app.name}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`${app.name}/index.html already updated`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(htmlPath, content, 'utf8');
|
||||
}
|
||||
|
||||
console.log('All PWA index.html files switched to precompiled bundles without Babel or unsafe-eval.');
|
||||
Loading…
Reference in New Issue
Block a user