Fix WASA security observations: security headers, CSP hardening, JS library updates, API authentication, and data scrubbing

This commit is contained in:
DevSoftTech Server 2026-10-19 16:27:17 +05:30
parent 8cbf55537f
commit c548c0dce3
24 changed files with 27656 additions and 19920 deletions

View File

@ -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,
];
/**

View File

@ -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

View File

@ -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,

View File

@ -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,

View 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;
}
}

View File

@ -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());

View File

@ -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">

View File

@ -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) {

View File

@ -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 ?>;

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

76
scripts/build_pwas.js Normal file
View 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!');

View 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.');